344 Commits
Author SHA1 Message Date
Claude 3faf78ec79 feat(issues): prototype canvas deliverable in the focus view
Before: an agent that built several HTML pages for a prototype could only
hand them over as separate files, and a reader had to open and page through
each one to see how the screens relate.

After: a `canvas.json` (or `*.canvas.json`) deliverable opens in the
full-screen focus as an infinite pan / zoom canvas of the issue's HTML
deliverables. Each frame shows the latest version of its HTML file in a
sandboxed iframe at its x / y / w / h, labelled with its name. Wheel or pinch
zooms around the cursor, dragging the background (or space + drag) pans, and
"Fit all" fits the page. A "Prototypes" list centers a frame, and Play opens
the frame's HTML full screen in the same focus, with "Back to canvas" in the
top bar. Frames can be dragged; "Save layout" uploads a new version of the
canvas file and posts it as a `/note` comment carrying the file card, so the
version joins the deliverable and wakes no agent. A new canvas version is
followed once loaded, and only frames whose HTML attachment changed remount.
The agent brief gets one optional paragraph describing the file shape.

How: `@multica/core/attachments/prototype-canvas` holds the pure parts (zod
schema with defaults, `parsePrototypeCanvas`, `moveFrame` on the raw JSON,
`fitAllTransform`, `centerOnFrame`, `zoomAround`, `isCanvasFilename`).
`AttachmentPreviewPanel` takes an optional `stage` that replaces the kind's
body while keeping the top bar and prev / next. The focus renders
`PrototypeCanvas` as that stage, tracks the canvas it was played from, and
follows new versions with a small `useFollowLatestVersion` hook that mirrors
the 4-D rule.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
2026-10-21 07:46:28 +00:00
Claude 77df149ed5 feat(issues): show a long document filling in section by section
Before: a long document an agent re-uploaded section by section showed up as
a series of versions; the full-screen view stayed on the version the reader
opened, rendered `<!-- pending -->` lines as nothing, and gave no sign of how
far the writing had got.

After: an outline upload whose unwritten sections are single
`<!-- pending: Title -->` lines renders them as dashed "Pending" placeholders.
While the focus is open on the latest version of a Markdown file, a new
version is followed once its text is loaded, the paragraphs it added carry a
label and a ~2s fading highlight, and the view scrolls to the first of them
if the reader was at the bottom. The header shows "Writing - n/m sections"
while an agent run is active on the issue and "Unfinished - n/m" once none
is. Agents are told the convention next to the `--attachment` guidance.

How: pure helpers in core (parsePendingSections, splitParagraphs,
diffParagraphs, writingProgress) with node tests. AttachmentPreviewPanel
takes an optional renderMarkdown; the focus passes ProgressiveMarkdown, which
splits only around placeholders and changed blocks and otherwise renders a
plain ReadonlyContent. Version text comes from useAttachmentHtmlText, the
preview's own query, and the running state from the workspace agent task
snapshot via selectIssueTasks. No backend change.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
2026-10-21 07:04:29 +00:00
Claude 13af0e9e3e feat(issues): show the agent's latest progress checklist in the feed
Before: The feed gave no sense of how far an agent had got. Agents could
not post a progress report that the feed understands: `comment add` had no
way to set a comment type, and the run brief told agents not to post
progress at all.

After: Each feed message shows a compact checklist under its thread bar
(done/total, a small bar, the first 5 items, "+N more"), and the issue peek
shows the same block under its title. Finished issues (done or closed
category) fold to a single "done/total" line with a chevron to expand. A
progress report is a `progress_update` comment by an agent whose body is a
Markdown task list; the newest one per issue wins.

How: The thread-summaries endpoint gains `latest_progress`
({comment_id, content, created_at} | null) from one batched DISTINCT ON
query, so there is no new endpoint. Core adds the type, a tolerant zod
schema, `parseProgressChecklist`, and a realtime patch for new reports.
Views add `FeedProgressCard`, mounted from the thread bar and the peek, with
strings in all five locales. `multica issue comment add --type
progress_update` lets agents post a report, and the run brief documents it as
the one exception to the single-comment rule.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_017deksZDSzZBA4QSqZZoBip
2026-10-21 06:10:12 +00:00
cfc165ce6a MUL-8095: fix(execenv): normalize required metadata for Codex user skills (#9346)
* fix(execenv): normalize required metadata for Codex user skills

Co-authored-by: multica-agent <[email protected]>

* fix(execenv): preserve user skill frontmatter formatting when filling metadata

Inject missing name/description lines instead of re-marshalling the map, so
comments, key order and CRLF endings survive; present-but-empty values are
replaced on the yaml.Node tree. Fix stale expected values in test messages.

MUL-8095

Co-authored-by: multica-agent <[email protected]>

* fix(execenv): only repair what Codex rejects and verify every repair

Codex falls back to the directory name for a missing/blank name and only
requires a non-empty description, so stop injecting name. The one-line
insert is kept for plain block mappings but now re-parsed; flow, indented
and other mappings go through the node rewrite, which expands aliases of a
replaced anchored value and keeps its comments. Every repair must parse and
leave all other fields equal, otherwise the skill is skipped.

MUL-8095

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: JC的AI分身 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-20 14:55:38 +08:00
88f6d6272b MUL-8057: feat(agent): run Meta Muse Code as an MSP runtime (#9309)
* feat(agent): run Meta Muse Code as an MSP runtime

Muse Code speaks MSP (Muse Session Protocol) over `muse serve`: JSON-RPC
2.0 on stdio, like ACP, but with its own method vocabulary. A session opens
with session/start or session/resume, work is submitted with turn/start, and
assistant text, reasoning and tool calls arrive as transcript items.

The adapter maps those items onto the Backend contract, reads the terminal
turn outcome, and reports a notFound resume as ResumeRejected so the daemon
retries on a fresh session instead of losing continuity silently.

Approval is a session property, not a launch flag: `muse serve` has no
--yolo, and the CLI documents --yolo as TUI-only. A fresh session is created
with approvalMode=allowAll and a resumed session re-asserts it with
session/setApprovalMode, which is what lets a claimed task run without
waiting for a human. The container and per-task worktree remain the
isolation boundary, as they are for every other backend.

commandId must be UUIDv7 — Muse derives a turn id from it and rejects
anything else — so the adapter mints time-ordered ids rather than v4.

The model catalog comes from the session-free model/list query and the pick
is applied through session/start modelId.

* feat(runtime-profiles): allow muse as a protocol family

A custom runtime profile may only be based on a protocol family the daemon
supports, and the database enforces the same list. Widening both keeps the
create flow, the server validation and the CHECK constraint in lockstep with
agent.SupportedTypes.

* fix(muse): complete runtime integration and lifecycle handling (MUL-8057)

Co-authored-by: multica-agent <[email protected]>

* docs(muse): document runtime setup and support (MUL-8057)

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-19 13:45:42 +08:00
700d25eef5 MUL-7420: render the workflow brief once on the server, for the assignee or an observer (#9293)
- issueworkflow.Brief is the only renderer: the claim carries it into every
  run's per-turn prompt; the editor preview and timeline show the same text.
- Workflow handoffs stop writing the brief into handoff_note, so a person's
  note no longer drops the brief and the handoff framing fits again.
- Agents that are not the issue's assignee get an observer brief without
  the step's commands.
- Clearer rule override and narrower wakeup guidance; user text is
  stripped of control characters.
- multica-platform skill documents project workflows.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-16 17:28:45 +08:00
da1671ca20 MUL-7420: Project workflows — columns and handoffs on the workflow (#8814)
* feat(workflows): workspace workflows with step handoffs (MUL-7420)

A workflow is a named, workspace-level selection of statuses from the
shared status catalog, in board order, with an optional handoff on each
step. Projects opt in through project.workflow_id; a project without one
keeps the whole catalog (the implicit Default workflow), so existing
workspaces see no change.

- issue_workflow table + project.workflow_id, CRUD API under
  /api/issue-workflows, POST /api/projects/{id}/workflow with a dry-run
  status mapping plan. Only issues on statuses the target workflow does
  not list are remapped, and remapping never hands issues off.
- Issue writes keep the status inside the project's workflow. Entering a
  step with a handler through a status change assigns the issue to that
  handler and starts its run with the step's brief as the handoff note;
  an explicit assignee in the same write wins. Creating an issue starts
  at the workflow's starting step.
- Task claims carry the issue's project workflow, and the brief renders
  a Project Workflow section so agents move along this project's
  statuses instead of the built-in ones.
- Statuses a workflow lists cannot be archived.
- Adoption is gated by the project_workflows_v1 flag; existing
  workflows keep working and projects can always switch back to Default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): workflow settings, project picker and workflow-aware boards (MUL-7420)

- Settings → Workflows (behind project_workflows_v1): the Default
  workflow plus named workflows, each shown as its step chain with
  handlers and the projects using it; an editor to pick statuses from the
  library, order them, mark the starting step, and set each step's
  handoff, instructions and next/back statuses.
- Project Workflow property: switching dry-runs first and asks where
  issues on unlisted statuses go; nothing else moves and nothing is
  handed off.
- A project board's columns are its workflow's steps, and each column
  head names who entering it hands the issue to.
- Status pickers offer only the issue's workflow steps and say who each
  hands off to; boards refuse drops onto statuses the dragged card's
  workflow does not list.
- New issues in a workflow project start on its starting step, with a
  hint naming who they will be handed to.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(workflows): name built-in steps once in the brief; roomier picker hints (MUL-7420)

Built-in statuses have no stored name, so the handoff brief printed
`todo` todo. Print the key alone when the name adds nothing.

The status picker widens for workflow projects and shows the handoff as
an arrow and name, so step names no longer truncate to one letter.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): handoff previews and a single timeline entry per handoff (MUL-7420)

- GET /api/issues/{id}/workflow-handoff?status= says what moving an issue
  would do: who it goes to, which runs of its current agent are still
  active, and the brief the handler receives. Backs the confirmation.
- POST /api/issue-workflows/preview-brief renders a step's brief from an
  unsaved workflow, for the editor's preview.
- Mapping plans list the statuses that stay, so the switch dialog can say
  which issues are untouched.
- A handoff is recorded on the status_changed activity (workflow, handler,
  whether a run started, the brief) instead of a separate assignee row.
- The brief now reads like the one shown in the design: the workflow, the
  current step, its instructions, the statuses, and the commands to hand
  off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): rebuild the workflow UI to the reviewed design (MUL-7420)

The first cut used generic controls where the design had purpose-built
ones. This brings every surface up to the mockup:

- Settings → Workflows is one page: the workflows (step chain with
  handler avatars, handoff count, projects using it; Default first) and,
  below, the shared status library with each status's workflow usage.
- The workflow editor is a full page instead of a dialog: draggable step
  cards summarising each handoff, and a panel for the selected step —
  swap its status, pick the handler with a segmented control and a
  searchable target, handoff instructions, next/back statuses, a preview
  of the brief the handler receives, duplicate and delete.
- The project Workflow picker lists each workflow's statuses, handoffs and
  projects, links to settings, and always confirms the switch with what
  moves and what stays.
- Moving an issue into a handoff step (status picker, table, board drop)
  asks first: who it goes from and to, a default-on option to stop the
  previous agent's run, and the brief.
- Status pickers name the workflow and project, show each step's handler
  with an avatar or that the assignee stays, and explain themselves.
- Boards: the handler chip in project column heads, hatched columns a
  dragged card cannot enter with the reason, and a drop hint naming who
  the card will go to.
- The timeline records a handoff under its status change, with the brief
  the run received; the issue sidebar shows the workflow it follows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(workflows): word a queued previous run as queued in the handoff confirm

The confirm dialog said the previous agent was "still running (for N
minutes)" even when its task had only been queued. Queued runs now read
as queued, and the checkbox cancels them instead of stopping them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): refuse a run's status change once the issue left its step

A run decides where the issue goes next from the step it was queued for.
When someone else moved the issue while the run worked, applying that
decision undid their move: in the live Grok run, a PM parked NW-1 as
blocked and the still-running agent moved it straight back to code
review.

Tasks now record the issue's status when they are queued
(agent_task_queue.workflow_step, written by the task INSERTs; automatic
retries keep their parent's step), and a run's own status changes move
it along. In a workflow project, an agent status change from a run whose
step no longer matches the issue is refused with 409
workflow_step_moved, naming where the issue is now and who moved it, so
the agent stops instead of retrying.

A handoff away from a squad now also stops the runs made on the squad's
behalf, and stopped runs record who stopped them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(workflows): keep every status entry point inside the issue's workflow

The issue context menu listed every catalog status and applied handoff
steps without the confirmation the status picker and board show, so a
right-click could pick a status the workflow refuses or start an agent
unasked. It now offers the workflow's steps and opens the handoff
confirmation through a shared modal.

One-click status actions no longer assume built-in statuses a workflow
may not list: "mark done" lands on the workflow's done step (hidden when
it has none), un-marking a duplicate returns to the workflow's start,
and "mark duplicate" is offered only when the workflow lists cancelled.
The batch toolbar offers a single project's workflow statuses when the
selection sits in one project.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(workflows): keep issue wakeups consistent with workflow steps

Three places where project workflows and issue wakeups met:

- A wakeup run is queued when it fires, so it took the issue's status at
  that moment as its workflow step. A wakeup an agent registered then
  bypassed the stale-step refusal: parked by a PM, the issue could be
  moved on by the agent's "check CI, then hand to review" wakeup. It now
  keeps the registering run's step; a member's wakeup still starts from
  the issue's status.
- Retries carried handoff_note only for wakeup runs, so a retried
  workflow handoff lost its brief. Rows with a workflow step keep it.
- Switching or editing a workflow remaps statuses in bulk without the
  close rule, so an issue remapped into a done status kept its wakeups
  and pending wakeup runs. The remap now ends them like any status write.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): don't also wake the agent a write hands the issue to

A workflow handoff or an assignment already starts the receiving agent's
run. If that agent also subscribed an event wakeup to the issue's status
or assignee, the same write queued a second run for one fact, and
workflows made such writes frequent.

The write now names the agent it hands the issue to in the
transaction-local multica.handed_to_agent (the new agent assignee, a
squad's leader, or a step's handler even when it already owned the
issue), and wakeup capture skips that agent's subscriptions for the
write's issue.* events. Other agents' wakeups, and task, comment and
reaction events, are captured as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): tell agents not to watch workflow issues with wakeups

The handoff brief and the runtime's project workflow section now say
that each step's handler gets a run when the issue enters the step, so
wakeups on a workflow issue are for waiting on time or on something
outside it, not on its status, assignee or runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(workflows): refresh project usage after project changes

Co-authored-by: multica-agent <[email protected]>

* feat(workflows): MUL-7420 split columns from per-project handoffs

Workflows now only define a board's columns between a fixed head
(backlog, todo) and tail (blocked, done, cancelled). Handoffs move to
project.workflow_handoffs, so projects can share columns and still hand
off to different people. Default becomes a real, editable row that every
project without a named workflow follows. Adds project handoff, add-column
(project fork or shared) and project brief preview endpoints; same-agent
adjacent columns re-run the agent.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(core): MUL-7420 project handoffs and Default workflow client model

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): MUL-7420 column-header handoffs and project workflow menu

Board column headers set the project's handoff for that column; the
project toolbar gets a workflow menu (full flow, switch, templates) and an
add-column action. Views outside a project show Default's columns, with
other statuses reachable through a status filter. Adds the issue flow card,
create-project workflow choice, a columns-only workflow editor and a
lighter status picker.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(views): MUL-7420 wrap create-project workflow label in a menu group

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): MUL-7420 keep workflow structure in settings; no squad handoffs

Projects no longer add columns or create workflows: the add-column
endpoint is removed and creating a workflow is admin-only. Handoffs go
to one agent, a member or the project lead; squads route work themselves,
so a handoff to one is rejected, as is the issue-creator handler.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* refactor(core): MUL-7420 drop the project add-column client

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): MUL-7420 simpler handoff picker and workflow editor form

The handoff form is one searchable "Hand to" picker (no handoff, the
project lead, an agent or a member); the next status defaults to the next
column and next/back overrides sit under "More options". The workflow
editor gets labelled name and description fields and shows the fixed
statuses as one start and one end row. Templates move from the project
menu to Settings → Workflows → New, and the project board loses its
add-column action.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* refactor(server): MUL-7420 keep handoffs on the workflow, make backlog and blocked optional

Handoffs move back onto workflow steps: a workflow is the whole pipeline,
and a project only picks one. The project lead stays a relative handler, so
projects can still share a workflow. Handoff edits are admin-only, through
the workflow write or PUT /api/issue-workflows/{id}/handoffs/{status}; the
brief preview moves to the workflow too. Drops the unreleased
project.workflow_handoffs migration.

Todo, done and cancelled are required; backlog and blocked are optional and
keep their fixed places. Without blocked, the brief points agents at the
step's back status.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Cursor <[email protected]>

* feat(views): MUL-7420 edit handoffs on the workflow, optional backlog and blocked

The workflow editor sets each column's handoff in its side panel, and
Backlog and Blocked switch on and off. The board's column popover and the
flow panel edit the workflow's handoff, say how many projects it reaches,
and are read-only for non-admins. Drops the per-project handoff client
code.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): MUL-7420 keep platform rules on writes that hand off nobody

Migration 579 skips an issue.* event for the agent a write hands the issue
to. Its filter compared agent_id with the transaction's handed_to_agent
setting using IS DISTINCT FROM, which is false when both are NULL. Main's
platform rules (child_done) have no agent_id, so every issue.* event that
handed the issue to nobody was dropped for them: a parent leaving backlog
never woke its agent for sub-issues that closed meanwhile
(TestChildDoneHeldWhileParentInBacklog).

Rules without an agent are never the handoff target, so they always
capture. Extends TestHandoffDoesNotAlsoWakeTheHandler with a platform rule
on a handoff and on a plain status change; the latter fails on the old
filter.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): MUL-7420 three-way handoff choice; quieter move confirm

The handoff form picks one of three directions, as in the mockup: no
handoff, hand to an agent (starts a run), or hand to a member, project lead
included (assignee only). What the step does, where it goes next and the
back status only reach an agent's brief, so they show and save for agent
handoffs alone.

The board's move confirmation keeps the agent's brief collapsed. Create
project shows the workflow picker even when Default is the only workflow,
with a note on where to make more. The hidden-columns panel says a board's
columns come from its workflow, with a link to manage workflows for admins.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(workflows): confirm handoff on project move, per-project handoff copy, two-pane editor (MUL-7420)

- A single issue moved into a workflow project from issue detail confirms
  the handoff of the column it lands on; the write opts in with
  enter_workflow_step. Batch, API and CLI moves stay administrative.
- The handoff preview takes project_id to preview such a move.
- Saving a shared workflow's handoff from a board can keep the change to
  that project: the server copies the workflow for it and switches the
  project in one transaction (copy_for_project_id).
- The column popover says which workflow it edits and who else uses it.
- The workflow editor is a step list with a detail panel for the
  selected step.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* style: gofmt workflow tests

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): MUL-7420 show next/back always; edit workflow name and description in place

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): MUL-7420 trim copy on the workflows settings page

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix: MUL-7420 keep pre-workflow behavior for workspaces that never adopt workflows

The run step guard now applies only to workflows that hand off, and the
standalone Statuses settings page keeps its original header.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(views): MUL-7420 use a solid placeholder tone on the workflow header

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(workflows): keep the runtime brief cache-stable; fix project board and batch status gaps (MUL-7420)

- Move the project workflow out of the runtime brief into the per-turn
  prompt so CLAUDE.md stays byte-identical across steps and issues.
- An explicit status filter overrides the project workflow's columns; the
  hidden-columns panel lists statuses outside the workflow.
- Batch status offers only statuses every selected workflow allows; a
  partial batch rolls back, refetches and warns.
- setProjectWorkflow returns void instead of an unparsed body.

Co-authored-by: Cursor <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Lambda <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-10-16 16:49:49 +08:00
Jiayuan Zhangandmultica-agent 231dedb552 MUL-8034: chore: clean historical comments across the codebase (#9274)
* chore: clean historical comments (MUL-8034)

Co-authored-by: multica-agent <[email protected]>

* docs: preserve pending Composio expiry contract

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-10-15 21:54:33 +08:00
8352787263 MUL-7658: fix(daemon): recover undelivered batch claims after timeout (ZIC-260) (#8774)
* fix(daemon): recover undelivered batch claims after request timeout

Co-authored-by: multica-agent <[email protected]>

* test(daemon): avoid copying task service locks in recovery fixture

Co-authored-by: multica-agent <[email protected]>

* test(tasks): isolate delegated recovery assertions (MUL-7658)

Co-authored-by: multica-agent <[email protected]>

* test(daemon): use one clock for deferred claim hints (MUL-7658)

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): keep claim recovery within client deadlines (MUL-7658)

Co-authored-by: multica-agent <[email protected]>

* fix(execenv): preserve racy Git index timestamps in snapshots (MUL-7658)

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): deliver claim responses before recovery side effects (MUL-7658)

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): isolate HTTP claim retries from recovery (MUL-7658)

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Sol-Boy <[email protected]>
2026-10-15 16:43:24 +08:00
6d5cdcbed2 MUL-7488: fix(execenv): exclude committed local HEAD changes from resume replay (#8549)
* fix(execenv): exclude clean HEAD advances from resume replay

* fix(execenv): normalize carried WIP before replaying local edits

Co-authored-by: multica-agent <[email protected]>

* fix(execenv): resume after conflicting local draft commits

Co-authored-by: multica-agent <[email protected]>

* fix(execenv): limit snapshot conflict handling to affected paths

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-14 14:43:47 +08:00
55c40d5db9 MUL-7982: resume Codex across continued local worktrees (#9218)
* fix(daemon): resume Codex across continued local worktrees

MUL-7982

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): serialize Codex session store writers

(cherry picked from commit 7cbe282e1ef5fa8d44e1cfd04def683ad259a360)
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: mameikagou <[email protected]>
2026-10-13 15:05:14 +08:00
92d0b6666d MUL-7812: fix(agent): deliver MCP servers to Pi tasks (#9063)
* fix(agent): deliver MCP servers to Pi tasks

Pi skipped untrusted project config in JSON mode, and Multica never wrote .pi/mcp.json, so composed MCP servers never reached the runtime.

Co-authored-by: Cursor <[email protected]>

* fix(agent): hand Pi its MCP servers through a temporary extension

Writing .pi/mcp.json and passing --approve trusted the whole project directory and failed when that file already existed. An explicit --extension loads the servers without project trust.

Co-authored-by: Cursor <[email protected]>

* docs: mark Pi as reading agent MCP configuration

The provider comparison still said Pi skipped the MCP field. Pi now receives that configuration.

Co-authored-by: Cursor <[email protected]>

* fix(agent): harden Pi MCP registration

Co-authored-by: multica-agent <[email protected]>

* fix(agent): allow empty Pi MCP configuration

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: Yushen <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-12 13:26:00 +08:00
756cdd915c MUL-7794 fix(wakeups): surface recoverable PR condition diagnostics [ZIC-302] (#9066)
* fix(wakeups): surface recoverable PR condition diagnostics

Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): preserve condition diagnostic ownership

Co-authored-by: multica-agent <[email protected]>

* test(wakeups): isolate receipt cleanup limit

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Yushen <[email protected]>
2026-10-12 13:15:46 +08:00
ZIceandmultica-agent fbd3470eac MUL-7875 fix(daemon): preserve Hermes sessions across worktrees (ZIC-301) (#9065)
* fix(daemon): resume Hermes sessions across task worktrees

Co-authored-by: multica-agent <[email protected]>

* fix(hermes): preserve unreadable history during migration

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-10-07 19:35:46 -07:00
b4ca5b4a23 refactor(skills): retire legacy issue skill redirect (#9046)
Co-authored-by: Multica Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-03 22:48:20 -06:00
Bohan Jiangandmultica-agent 43b0571f99 MUL-7821: feat(cli): mark duplicates with issue status/update --duplicate-of (#8979)
* feat(cli): mark duplicates from issue status and issue update (MUL-7821)

Add --duplicate-of to `multica issue status` and `multica issue update`.
It resolves an issue key or UUID and sends duplicate_of_issue_id, which
the server already accepts (MUL-7349). The CLI rejects a status other than
cancelled, and description or attachment changes, before any request or
upload. It fails when the response has no duplicate_of field, so a server
older than v0.5.2 cannot silently drop the mark.

Closes #8974

Co-authored-by: multica-agent <[email protected]>

* docs: teach agents to mark duplicates with --duplicate-of (MUL-7821)

Add the command to the agent runtime brief, the multica-platform issues
reference, CLI_AND_DAEMON.md and the CLI docs tables, so agents record the
mark instead of cancelling a duplicate and explaining it in a comment.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-09-29 23:27:27 -07:00
5ed09342af MUL-7680: issue wakeup v2 — conditions, runaway protection, check-ins and visibility (#8807)
* feat(wakeups): add rule deadlines and expose the child-done system rule (MUL-7680)

Wakeups can now end: an absolute deadline (expires_at) or a relative wait
(expires_in_seconds) that restarts when the rule is re-enabled. When the
deadline passes first, the scheduler ends the rule; event rules with
on_timeout=wake run the target once with a wakeup.timeout fact. A timed-out
rule keeps disabled_at NULL so its timeout run stays claimable.

The implicit "wake the parent's assignee when a stage of sub-issues
finishes" behavior is now described by GET /api/issues/{id}/system-wakeups
and can be turned off or given a supplementary instruction per issue via
PUT /api/issues/{id}/system-wakeups/child_done. Rule reads fail open to the
previous behavior.

List responses add the creator (member or agent) and the new expiry fields.
The CLI gains --expires-in, --expires-at and --on-timeout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(issues): let members create wakeups and show the child-done system rule (MUL-7680)

The issue sidebar's Wakeups section is always available on open issues and
gains a New wakeup popover. Members pick a condition (a time, a recurring
check with an end date, someone's reply, an agent's run ending, or raw
events), the agent to wake, the instruction, how often it fires, how long
to wait and what happens on timeout. It posts the same configuration agents
create with the CLI.

The parent's child-done wake appears as a System rule with its stage,
remaining sub-issues and target, a per-issue toggle and a supplementary
instruction. Rows show when a rule ends, timed-out rules read as such, and
details name who created the rule. Rule titles wrap to two lines instead of
truncating.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* docs(wakeups): document deadlines, member-created rules and the system rule (MUL-7680)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(wakeups): platform conditions, runaway protection and check-ins (MUL-7680)

Conditions: a wakeup can now wait for a fact the platform checks itself
(a status, assignee, label or property value; sub-issues or one stage
finishing; a linked PR's checks finishing or merging; another issue's
status). Related events only prompt an early evaluation; the rule wakes
its agent when the predicate becomes true, and a repeating rule fires
again only after it turned false or its facts changed. Finished PR checks
from before registration are ignored.

Runaway protection: repeating event rules stop after max_fires runs
(default 20), a rule pauses when its causal chain passes through it a
third time without a person in between, or when it starts 12 runs in an
hour. The reason is stored and shown.

Also: silent check-ins for every/cron checks (the run then posts no
fallback comment), wake now, delete, a per-rule run history, paused-rule
lists, timeline entries for created/triggered/timed-out/paused/check-in,
and source, paused scope, 7-day runs and child-done system rows in the
workspace list. The CLI gains --until-* conditions, --max-fires and the
trigger/delete/checkin/runs commands; the brief and wakeup prompt state
the check-in exception.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(issues): record child-done transitions in the status write (MUL-7680)

The child-done system rule ran after the status write committed and gave
up on any error, so a crash, deploy or transient failure lost the
parent's wake. A trigger now records each child's move into a closed
status in the writing transaction, for every writer. The request that
wrote it processes the rows right away and a scheduler sweep retries
anything left over; claims make the two paths process a transition
once.

The rule also follows a workspace default (settings key
system_wakeup_child_done) until an issue sets its own, the per-issue
update accepts partial bodies, and each trigger adds a timeline entry
that names its system comment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): wakeup conditions, history, timeline and workspace list (MUL-7680)

Issue sidebar: members can create the platform-checked conditions (a field
reaching a value, sub-issues or a stage finishing, a linked PR's CI or merge,
another issue's status) and cap how often a repeating wait fires. Rule
details show the fire count, why the platform paused a rule, the latest runs
with silent-check notes, and offer wake now and delete.

The issue header says what the issue is waiting for ("Emacs 在等 Jiayuan 回复
+2") and opens the Wakeups section. Board and list cards use the same short
sentence, or flag a paused rule. Wakeup runs read as "由唤醒触发 · <condition>"
in the execution log, transcript and on the comments they post.

The timeline shows created, triggered, timed-out, paused and check-in entries
with the rule they came from; consecutive check-ins merge, and the child-done
entry stands in for its system comment, which it can reveal.

Automation → Issue wakeups adds a source column and filter, 7-day runs, a
paused scope with a banner, the child-done system rule on each waiting parent,
and "New wakeup" with an issue picker. Settings → Issue statuses sets the
system rule's workspace default. Copy in all five languages.

The server records the watched issue's identifier in other-issue conditions
and the rule's creator in timeline snapshots, and returns board summaries with
their condition.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* docs(wakeups): document conditions, runaway protection and check-ins (MUL-7680)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): list system rules without a revision and localize statuses (MUL-7680)

System rule rows returned revision 0, which clients reject as a rule
revision; they now return null, and a list row with an invalid revision no
longer fails the whole page. Built-in statuses in conditions and the status
picker read in the viewer's language. Adds a browser test for a condition
created from the form, the header summary, the scheduler waking the agent
and the workspace list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(views): say why a rule paused without repeating "paused" (MUL-7680)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(i18n): use the product's task terms in wakeup copy (MUL-7680)

ja, ko and fr wakeup strings said イシュー / 이슈 / ticket; the product term
for an issue in those languages is タスク / 태스크 / tâche (and sub-tasks
accordingly), per the conventions page. French strings are rewritten for
the feminine noun.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* test(views): pin the shortcut platform in the wakeup form test (MUL-7680)

The send shortcut is primary+Enter, which is Ctrl on Linux CI runners, so
pressing Cmd+Enter only submitted on macOS.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(views): simplify the workspace wakeup list

Put the filters on one row: scope as a segmented control on the left,
source/trigger/agent and a search-as-you-type box on the right. "New
wakeup" moves to the page header, which drops the separate Search button.

Rows are single-line: the identifier sits before the title, frequency and
timezone move into the trigger tooltip, and selection, prompt edit and
the last run's transcript show on row hover or focus. The issue column
takes the remaining width and truncates, so the table fits the page.

Also fix creating from the list: the issue picker closes itself right
after reporting the selection, and that close cancelled the flow before
the form could open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* refactor(server): run the child_done rule as a system issue wakeup

The parent-assignee wake on closed sub-issue stages was its own code path:
a system comment, a mention-style run, an override table and endpoints.
It is now an ordinary issue_wakeup row with system_rule set, sharing the
condition, receipts, runaway protection, timeline entries and run model of
people's rules.

- One children_done evaluator: a stage wakes the assignee when it closes
  while a later stage waits; the wrap-up waits for every sub-issue,
  unstaged ones included. People's sub-issue conditions read the same set.
- issue_child_event records closing, reopening, re-parenting and restaging
  for every writer; the request processes it right after commit and the
  sweep retries. User sub-issue rules become immediate too.
- The target is resolved when it fires: an agent run, a squad leader run,
  an inbox notification for a member, or only the timeline entry.
- A parked (backlog) parent catches up when it leaves backlog; a waiting
  run of the same agent is joined instead of queuing a second one; the
  hourly limit pauses a runaway rule. No system comment is posted.
- The run gets the issue's instruction, else the workspace's, else the
  built-in one, plus each stage's counts and the next stage.
- Workspace defaults move to GET/PUT /api/system-wakeups (owners and
  admins) and apply to every rule nobody customized; turning a rule on
  records what already holds so old facts do not fire.
- Existing open parents get their rule from a one-time backfill.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): workspace wakeup settings and sub-issue notifications

- Settings → Wakeups holds the sub-issue rule's workspace default and
  default instruction; the toggle leaves Issue statuses.
- The issue's system rule shows who it reaches (a member is notified),
  a platform pause, and the instruction it will use.
- Timeline entries say whether the assignee was woken, notified or joined
  a waiting run. The folding of the old system comment is gone.
- Inbox renders the new children_done notification on web, desktop and
  mobile.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(server): stop wakeups from repeating their agent's runs

Runs are serialized per issue and agent, so a wakeup never ran beside
another run of its agent, but it queued behind one and repeated it. Each
rule now checks two things before it starts a run:

- Acknowledged: every input came from the agent itself. Its own comments
  and issue changes never wake it, and a condition its own unfinished run
  on the issue satisfied does not wake it when the platform or the agent
  set the rule up. This fixes #8849: a coordinator closing its own stage is
  not woken again while that run is going. A person's condition rule still
  runs afterwards, since the running agent lacks its instruction.
- Merged: when a run of the agent is already waiting to start on the issue
  (assignment, comment, another rule), the rule's instruction and facts
  join that run (context.wakeup_joined, sent to the daemon as
  wakeup_joined and rendered for every prompt kind) instead of queuing
  another. Turning the rule off or replacing it withdraws what it joined.

Sub-issue changes now record the run that made them, and hints and
condition.met inputs carry those runs so a satisfied condition knows
whether the agent caused it. A rule that names the agent itself as the
actor keeps waking it on its own changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(views): show merged and acknowledged wakeups, hint assignee comment rules

- Timeline entries say when a wakeup joined the agent's waiting run or was
  the agent's own doing, for people's rules and the sub-issue rule.
- The create form tells a member that a reply or comment rule for the
  issue's agent assignee joins the run a comment already starts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(server): hand waiting wakeups to a run when it is claimed

A rule that fired while its agent had a run waiting used to write its
instruction into that run right away and consume its inputs. That let a
member's rule run under another member's identity, kept deleted rules'
instructions in the run, lost the inputs when the run was cancelled or
claimed by an older daemon, and skipped the fire cap and loop check.

Now the rule only waits for a run that runs as the same person its own
run would, keeping its inputs. When a daemon advertising
joined-wakeups-v1 claims that run, JoinWaitingWakeups rechecks each rule
(on, same agent and person, creator still allowed, not the agent's own
input, no loop), consumes its inputs, counts the fire toward max_fires,
adds its chain and records the merge. A re-claim drops entries of rules
turned off or changed since. Anything else leaves the rule its inputs to
start its own run.

A child_done rule created by the backfill or by processing a change now
treats sub-issues with unprocessed close events as still open, so the
backfill no longer swallows a close waiting for the sweep.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(server): consume joined wakeup inputs only once the run starts

A claim used to consume the inputs of the rules that joined the run. If
that claim did not go through, the run went back to the queue carrying
inputs the rules no longer had: turning the sub-issue rule off on the
issue or for the workspace (which only changes enabled) did not remove
them from the next claim, and cancelling the run lost a once rule that
had already ended.

A claim now reserves the inputs for the run (receipt task_id, still
unprocessed). The rule's next dispatch settles them: a run that started
consumes them as a merged firing (once ends, max_fires counts and can
pause, timeline entry); a run that ended without starting gives them
back; one that has not started keeps them. Turning a rule off or
changing it discards its pending inputs, reserved ones included, and a
later claim of the same run rechecks every entry and drops the ones
without reserved inputs or no longer allowed to reach the run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-27 21:08:54 +08:00
7dac88a6a8 MUL-7649: issue deliverables and dynamic blocks — sidebar section, overview grid, viewer info panel, html/mermaid block frame (#8779)
* feat(attachments): issue deliverables model and sequence block ids (MUL-7649)

collectDeliverableFiles groups every comment upload into deliverables,
merging same-name, same-type re-uploads into versions (newest version
first). Description attachments are inputs and never enter the model.

Sequence items now carry the id of the block they came from, so a viewer
can say which comment (or the description) a file was posted in.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(viewer): host details, overview and info panel slots (MUL-7649)

PreviewSequenceProvider takes an optional describeItem (title accessory,
info panel, locate action) and onOpenOverview. The viewer renders only the
slots it is handed: a locate button, a grid button (G) and an info toggle
(I) whose panel sits beside the stage. Letter keys stand down in fields,
and any key pressed inside an open menu stays with the menu.

A zoom canvas still at fit now follows a viewport resize (the info panel
opening), while a zoom the reader chose is only clamped, as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(issues): deliverables section, overview grid and info panel (MUL-7649)

The sidebar's "Deliverables" section shows what the issue delivered as a
whole: its pull requests and the files its comments uploaded, re-uploads
merged into one entry marked v2. It lists the three newest images and four
newest files, plus "View all N deliverables".

The former Pull requests section becomes the code group, keeping its
MUL-7429 actions (link a PR by URL, the auto-complete menu) beside the
group label. While the workspace shows PRs, the section stays up even with
nothing delivered, since that is where a PR gets linked by hand.

The overview (from "view all" or G in the viewer) shows code first, then
files grouped by posting comment, filterable by kind, each group one click
from its comment. Its count always equals the sidebar's.

In the viewer, deliverables get a version switcher, and every file gets an
info panel (source excerpt, sibling files, uploader, time, size) and
"Show in comments", which unfolds a resolved thread if needed and reuses
the timeline's jump-and-flash highlight.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(comments): standalone attachments as cards and an image row (MUL-7649)

The files under a comment (and a chat message) no longer stack as one
full-width row each. Other files become cards in a grid: type glyph, name,
"TYPE · size", download / remove on hover, and the whole card opens the
file. Several images sit in one row at a shared height; a lone image keeps
its full size. HTML keeps its embedded preview (MUL-2330).

On the issue page a re-uploaded file's card carries its version (v1, v2)
from the deliverables model, through a small context.

Standalone attachments are grouped images, then HTML, then files, and the
preview sequence walks them in that same order, so paging follows the
screen. The file-type glyph moves to editor/utils/file-icon so comment
cards and the deliverables surfaces share one mapping.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* refactor(issues): pull requests keep their own section (MUL-7649)

Since MUL-7429 the PR block is issue workflow — linking a PR by hand, the
auto-complete switch, "won't auto-complete: missing Closes" — rather than
output, so it goes back to main's own Pull requests section, unchanged,
directly above Deliverables.

Deliverables is now the delivered files only: hidden until a comment
delivers one, and the sidebar number, "View all N" and the overview's
"All N" all count files. The overview drops its code group.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(comments): lay several images out as justified rows (MUL-7649)

Fixed-height tiles wrapped as soon as the column narrowed — three
screenshots became two plus an orphan, with a ragged right edge. Rows are
now justified from each image's real aspect ratio: every row shares one
height and a full row runs edge to edge. A row takes as many images as fit
above a minimum height, never grows past a maximum, and a last row that
does not fill keeps the height of the row above it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(rich-content): dynamic block frame for html and mermaid fences (MUL-7649)

A fenced ```html or ```mermaid block is part of the message body, so it now
renders in one shared frame instead of two unrelated widgets:

- Always-visible title bar: kind icon, the fence's title="..." (or the kind
  name), a kind chip, Preview | Source, fullscreen and copy. Previously the
  controls only appeared on hover and floated over the content.
- The body takes its content's height (at least 120px) and collapses past
  480px behind a fade and "Show all". An HTML block used to be a fixed 480px.
- Loading keeps the height this block had earlier in the session; a script
  error or a Mermaid parse error is explained inside the frame with
  "View source" and "Copy error".
- HTML gets the app's theme tokens (--foreground, --chart-1 ...); HTML that
  uses one also gets the app's color-scheme, so it follows dark mode. HTML
  that uses none keeps its own look.

The sandboxed document reports its height and first uncaught error through a
one-line postMessage bridge; the page checks the message source and clamps
every value, and stops a document whose height follows the frame. rehype-raw
drops the fence meta, so the title is read in the closed-fence parse.

The Mermaid sandbox now also declares the app's color-scheme and font: in
dark mode it was painted on an opaque white canvas, and its labels were drawn
in a serif font that did not match the layout Mermaid measured.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(attachments): HTML files show as cards, not inline previews (MUL-7649)

Reverses the MUL-2330 pin. An uploaded HTML file is a deliverable to open,
not part of the text: like every other non-image file it shows as a card
(a row inline in the body, a grid card under it) that opens in the viewer,
and its contents are no longer fetched to embed a 480px iframe. HTML meant
to be read in place is written as a ```html block, which renders as a
dynamic block.

- Attachment drops its html branch; HtmlAttachmentPreview is deleted and
  HtmlPreviewBody keeps only the inline source the viewer uses.
- orderStandaloneAttachments groups images, then files (HTML included), so
  AttachmentList and the viewer sequence still walk the same order.
- The MUL-2330 regression pins now assert the card and that no text fetch
  happens.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* docs: charts go in html/mermaid blocks, files show as cards (MUL-7649)

Agents that uploaded an HTML chart expecting it to render in the comment now
get a file card instead. Tell them where each kind of content goes:

- multica-platform skill (issues reference, routed from the table): a fenced
  ```html / ```mermaid block renders in place with a title bar and content
  height; an attached file, HTML included, is a card that opens the viewer.
  Covers title="...", the sandbox, the theme variables and their opt-in
  color scheme, and sizing to content rather than the viewport.
- `multica issue comment add --attachment` help says the same in one line.
- Comments docs (all five languages) describe both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* feat(daemon): runtime brief says where charts and diagrams go (MUL-7649)

The chart/file rule lived only in the multica-platform skill's issues
reference, which an agent opens on demand, and the skill's description did
not mention comment formatting, so an agent that just wanted a chart in its
result had no reason to read it. Agents that uploaded report.html expecting
an inline chart now get a card.

- The brief's Output section carries one line, beside the file-delivery
  line, on the surfaces the web renders (issue comments and web/mobile
  chat): put charts and diagrams in the text as a fenced html or mermaid
  block, name it with title="...", and an attached file, HTML included,
  shows as a card. Theming and sizing stay in the reference.
- Channel chats, autopilot run results and quick-create stdout do not render
  these blocks and do not get the line; the delivery tests pin both sides.
- The skill description names "charts and files in comments" so the
  reference is picked for that task.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(issues): locate a file in a collapsed thread (MUL-7649)

"Show in comments" for a file posted in a reply only unfolded resolved
threads and gave up after 30 frames, so a thread the reader had collapsed
stayed shut and the reply never mounted. A reply now goes through the
quick-jump rail's jumpToReply, which already undoes every kind of folding
and waits for the reply to land. A root comment gets the same treatment for
its own thread (the reader's collapse, or a resolved bar), then the jump.

Found in review by Emacs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(viewer): the info panel's "Show in comments" closes the viewer (MUL-7649)

The top bar's locate went through the viewer, which closes itself first;
the same button in the info panel called the host's callback directly, so
the page scrolled underneath a viewer that still covered it. Controls handed
to describeItem gain close(), and the issue page builds one close-then-locate
action for both entry points.

Found in review by Emacs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

* fix(issues): the deliverables overview is a real modal dialog (MUL-7649)

The overview was a portaled layer with role="dialog" and nothing else: focus
stayed on the opener, Tab walked into the page underneath, and closing left
focus wherever it was. It now renders through the shared Dialog, restyled to
cover the window, so the primitive moves focus in, keeps it inside, returns
it to the opener, and handles Escape. `G` back to the viewer's file stays.
The dialog is named by its title ("MUL-123 deliverables").

Found in review by Emacs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-27 03:23:21 +08:00
e909e9c89d MUL-7587 fix(daemon): show on-behalf-of identity in agent context (#8688)
* fix(daemon): expose original requester in agent context

* fix(daemon): render run originator as on-behalf-of identity

* docs(daemon): update stale Task Initiator comments to On Behalf Of

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-24 15:35:54 +08:00
bojackli df2e86cf3a fix(daemon): retry Windows task root record rename (#8786)
Refs #8785
2026-09-24 15:10:44 +08:00
pseudoyuandClaude Fable 5.1 62efe26aa0 MUL-7620 feat(telegram): send and receive photos, videos and files (#8727)
* feat(telegram): send and receive photos, videos and files

Telegram was text-only in both directions: dispatch dropped every non-text
update with an "unsupported" notice, the outbound subscriber only knew
sendMessage/editMessageText, and the channel was never declared to
DeclareChannelFileDelivery, so agents were told they cannot attach files.

Inbound: photos, documents, video, video notes, animations, audio and voice
notes become chat attachments through the engine's MediaResolver seam
(getFile, file host download, object storage, intent-ledger row before the
PUT, as Slack does). The caption is the message text and a placeholder marker
leads the sender's own text; the engine swaps it for the attachment link once
the bind commits. Quoted and recent group context still go in front of it.
Stickers and other non-file kinds keep the unsupported notice.

Outbound: files the agent bound to its reply are sent into the chat as their
own messages once the text settles (sendPhoto for common images up to 10 MB,
sendVideo/sendAudio for mp4 and common audio, sendDocument for the rest up to
50 MB). A photo Telegram refuses to process is resent as a document.

Both halves hang off the same store != nil branch that declares file
delivery, so the agent is promised the hop only where it exists. Compared
with the WeCom reference this drops relay routing, the three-state delivery
classification, per-reply metrics and the pending/admitted counters: Telegram
outbound is stateless HTTP and the delivery lease already guarantees a single
sender per turn.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* fix(telegram): address media review — deliver files once, admit before spawning, keep the notice without storage

Review on #8727 raised five points; each lands here with a test.

1. A file-only reply could be delivered twice. closeTurn returned true
   whether this call ended the turn, a deeper attempt owned it, or it was
   settled already, and the empty-reply branch delivered files on all three.
   closeTurn now reports closedNow / closedAlready / closeHeld, and files go
   out only on closedNow. Regression tests: a replayed chat:done on another
   replica sends the files once; a completion an automatic retry has
   superseded sends nothing and the retry's answer still lands.

2. Admission sat behind the goroutine and the lookup. The attachment lookup
   now runs on the terminal worker, so a reply with nothing bound costs one
   indexed read and no goroutine; a delivery is spawned only for files known
   to exist, under a slot claimed first (non-blocking, four slots). Every
   slot busy sheds the reply with the notice instead of parking it.

3. Rebased on main over MUL-7585: recent group context, /new isolation and
   the privacy rule are kept; unaddressed group media is buffered and never a
   turn, as before; the media placeholder leads the sender's own text with
   quoted and recent context still in front.

4. Without object storage the member got no signal. The polling loop now
   knows whether the deployment stores media (ChannelDeps.AcceptsMedia, set
   from the same store != nil branch as the resolver) and keeps the old
   unsupported notice when it does not. A fetch that fails — over the 20 MB
   bot download limit, or a failed download — tells the sender so, instead of
   leaving the agent a placeholder nothing stands behind.

5. The failure notice claimed more than the code knew: a lost response may
   mean Telegram accepted the file. It now says delivery could not be
   confirmed and that the file stays attached in Multica, which holds either
   way.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* fix(telegram): offset the media placeholder past prepended context; report a failed attachment lookup

Second review round on #8727.

1. With quoted or recent group context ahead of the sender's text, the
   MediaRef carried only the placeholder, so the engine replaced occurrence
   0: a member who typed "[Image]" in the window received the sender's
   file and the sender's own marker stayed bare. inboundMedia now carries
   PlaceholderIndex — the enrichers only prepend, so it is the count of the
   marker ahead of the sender's segment, literals included (the DingTalk
   rule) — and the resolver sets it as InlineIndex. Regression tests cover
   the recent-context and the quoted-message paths.

2. A failed ListAttachmentsByChatMessage dropped the files silently while
   the text already on screen referred to them. The read has no side
   effects, so it is retried three times 250 ms apart; if it still fails
   the member gets a notice worded for what is known — whether the reply
   had files could not be checked — never the one that presumes a file
   existed.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
2026-09-24 12:41:47 +08:00
Bohan Jiangandmultica-agent 81f0fb418a test(execenv): stop the test repo template racing git auto maintenance (#8730)
The shared template repo is built once per test binary and handed to every
test as an os.CopyFS copy, so nothing may write inside it after
buildTestRepoTemplate returns. git commit breaks that: it forks a detached
`git maintenance run --auto` and returns without waiting, leaving a
background process that creates and removes lock files under .git/.

CopyFS then lists .git/objects/maintenance.lock and fails to open it once
maintenance has removed it, failing whichever test happened to call
newTestRepo at that moment — most recently
TestGitRootLockTimeoutDoesNotAdviseDeletingTheLock on an auth-only PR
(#8706), which touches nothing in this package.

Disable auto maintenance in the template's config. Copies inherit it, so the
git commands tests run against their own copy cannot spawn a background
process that races their assertions either.

Co-authored-by: multica-agent <[email protected]>
2026-09-23 14:38:02 +08:00
d2423aa35c MUL-7590: fix(execenv): support OpenClaw agents.entries per-agent workspaces (#8678)
* fix(execenv): pin per-agent workspaces under the OpenClaw agents.entries schema

OpenClaw 2026.8 retired the agents.list config path and reports it as
"Unknown config path: agents.list", moving the agents to a map keyed by agent
id under agents.entries. The per-task wrapper only ever wrote
agents.defaults.workspace, and an agents.entries.<id>.workspace entry outranks
it verbatim: on a host where adding a second agent pinned the gateway agent to
an absolute workspace, every task ran in that pinned directory instead of its
prepared workdir, so the workdir skills/ never loaded and the agent edited the
wrong tree.

Read agents.entries when agents.list is gone, pin every entry workspace to the
task workdir on write-back, and keep the registry fallback for the generations
that have neither config path. The stdout-envelope branch now matches the
path-aware wording, so a 2026.8 host that prints its envelope without exiting is
read instead of failed closed, and the entries probe shares the agents.list
deadline so the worst case stays three CLI deadlines.

Co-authored-by: multica-agent <[email protected]>

* test(execenv): cover the agents.entries probe, write-back and shared deadline

Adds the regressions the review of the earlier attempt asked for: the current
"Unknown config path" envelope with err == nil selects the entries probe and
never the registry, both the plain and the managed-MCP flows pin every
agents.entries.<id>.workspace to the task workdir, a valid-but-unset entries
path falls back to the registry, and an envelope naming an unrelated path still
fails closed.

The worst-case budget test now drives the five-invocation chain and pins the
config-schema pair to one shared deadline, so a fourth budget fails loudly.

Co-authored-by: multica-agent <[email protected]>

* fix(execenv): pin agents.entries workspaces without copying the entry

Review feedback on the write-back: `config get` redacts the config before it
returns the path it was asked for, so every sensitive per-agent field comes back
as `__OPENCLAW_REDACTED__`. The wrapper is a sibling of the user's config in the
$include merge and siblings win, so copying the resolved entry would write that
sentinel over the user's real secret — memory search API key, web fetch headers,
sandbox SSH credentials — for every task on the host, and nothing on OpenClaw's
load path restores it. `agents.entries` is an object keyed by id and objects
merge recursively, so `{"<id>": {"workspace": "<workdir>"}}` is enough to pin the
workspace while every other field still arrives from the user's own config.

The same reasoning drops the resolved payload from the row representation: an
entries row now carries the id marker alone, so no redacted copy of the user's
agent config is held in memory or written to the discovery cache file.

Since only the source still decides the write-back, it is now carried
explicitly (`openclawAgentsSource`: list / entries / registry) rather than as a
boolean that effectively meant "not agents.list", and the source is recorded in
the cache entry so a hit rebuilds the same wrapper. The cache shape version is
bumped, so an entry written by an older daemon is a clean miss.

Co-authored-by: multica-agent <[email protected]>

* test(execenv): assert the entries write-back emits workspace alone

Review feedback: the tests encoded the old contract (name / model carried into
the wrapper verbatim). They now assert the wrapper carries exactly one key per
entry — `workspace` — and that nothing else from the resolved payload reaches
the file OpenClaw reads.

The new regression drives the redaction shapes upstream's own redact-snapshot
suite covers (memory search apiKey, web fetch headers, sandbox SSH credentials)
through the entries probe of a real preparation, and fails if any of them ends
up in the wrapper: with the copying write-back restored it reports the sentinel
in `agents.entries.main`. The unit test for the write-back gate also covers a
half-marked list, which a quiet per-row filter would have pinned anyway.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: tianshuai1 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-22 16:22:28 +08:00
Multica Eveandmultica-agent 5140b99d86 MUL-7577: remove no-start guidance from runtime and platform skill (#8680)
* fix(prompts): remove no-start guidance from runtime briefs

Co-authored-by: multica-agent <[email protected]>

* fix(skills): remove no-start guidance from platform instructions

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-09-22 13:27:52 +08:00
Multica Eveandmultica-agent 8c9f865e3e MUL-7480: Revert comment steering feature (#8648)
* Revert "MUL-7480: Link delivered steers to final replies (#8623)"

This reverts commit 3de6275eb3.

Co-authored-by: multica-agent <[email protected]>

* Revert "fix: preserve active task intent in steer frames (#8621)"

This reverts commit b866dacd1c.

Co-authored-by: multica-agent <[email protected]>

* Revert "MUL-7480: Steer active agent turns from comments (#8605)"

This reverts commit 8ce795b007.

Co-authored-by: multica-agent <[email protected]>

* chore(db): remove reverted comment steer schema

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
2026-09-21 18:29:50 +08:00
Bohan Jiangandmultica-agent 624648cc14 fix(daemon): offer a Git Bash variant of the Windows reply cookbook (MUL-7554) (#8634)
The Windows brief only knew PowerShell: the per-turn reply cookbook and
the Comment Formatting cleanup line assumed every Windows agent pastes
into PowerShell. The daemon knows the host OS, not the shell behind the
agent's command tool. Claude Code runs Git Bash for its Bash tool, and a
user who turns off its PowerShell tool (CLAUDE_CODE_USE_POWERSHELL_TOOL=0,
GitHub #8627) gets a cookbook whose 'if ($LASTEXITCODE ...)' line is a
bash syntax error that fires AFTER the post succeeded, so the agent sees
a failure and may re-post.

Render one variant per shell on Windows (PowerShell with the
$LASTEXITCODE gate, Git Bash with '&&') and tell the agent to pick the
one its command tool runs. The shell-execution test now selects the
block by shell and adds a windows/git-bash case that runs the bash
variant under sh.

Co-authored-by: multica-agent <[email protected]>
2026-09-21 16:46:52 +08:00
VvVandweiweiwei 2b62b2b52e fix(cli): compare workdir paths in one namespace (MUL-4252, MUL-7193) (#8028)
* fix(cli): compare workdir paths in one namespace (MUL-4252)

The containment guard behind --content-file, --description-file and --attachment
canonicalizes the workdir but could not canonicalize a candidate path that does
not exist yet, and compared the two sides in different namespaces. That is wrong
in three ways, and only the first is merely a wrong answer.

## A path inside the workdir read as outside

filepath.EvalSymlinks fails outright when any component is missing. The fallback
resolved one level up (filepath.Dir) and otherwise called filepath.Clean, which
does not follow symlinks at all.

os.Getwd() returns the LOGICAL working directory whenever $PWD names the current
directory, and everything that writes $PWD carries the path as typed: a shell's
`cd`, and the PWD=<workdir> this repo exports to agent processes
(pkg/agent/opencode.go, deveco.go, codearts.go). So on a host whose workdir path
crosses a symlink, the base was /private/var/... while the candidate stayed
/var/..., and `--content-file reports/summary.md` was rejected as a guardrail
violation when the real problem was that `reports/` did not exist. The only
remedy the error offers is --allow-external-file, so the caller's next move is to
disable the guard and collect an unrelated not-found error on the retry.

## A path outside the workdir read as inside

With an already-canonical workdir the same fallback inverts. `escape/sub/x.md`,
where `escape` is a symlink pointing out of the workdir, read as INSIDE it: the
unresolvable tail fell back to a lexical clean that never sees the symlink.

## ".." was applied to the string, not to the followed path

filepath.Join and filepath.Clean collapse "escape/.." before any symlink is
followed, while the kernel follows `escape` out of the workdir first and then
goes up. So the guard judged a different path than os.ReadFile then opened:

  guard says within=true ; os.ReadFile got "another run's file"

That is the MUL-4252 failure this guard exists to prevent — reading a file left
behind by another run — reachable with a file that exists and no race.

## A path on another volume read as an internal failure

Windows only: filepath.Rel cannot relate two absolute paths on different volumes
and returns an error, which surfaced as `Rel: can't make Z:\report.md relative to
C:\Users\...\workdir`. Two absolute paths Rel cannot relate are on different
volumes, which is as far outside the workdir as a path gets, so the guard now
answers that instead.

## The fix

util.ResolveSymlinksBestEffort canonicalizes as far as the filesystem allows: it
resolves the path as given first — the only step that can see ".." the way the
kernel does — then walks up to the deepest existing ancestor, resolves that, and
re-attaches the missing tail lexically. A mismatch inside the missing tail is
inert: a path whose intermediate directories do not exist cannot be opened at
all. Both sides of the comparison go through it.

internal/daemon/execenv already had this algorithm as resolvePathBestEffort,
unexported and documented as Python's Path.resolve(strict=False). It now
delegates, so there is one owner, and the ".." handling makes it more faithful to
that documented contract rather than less.

## Coverage

TestFileWithinWorkingDir runs its table under two working-directory shapes, and
both are load-bearing: under a symlinked workdir an unresolved candidate always
looks outside, so the symlink-escape rows can only fail under a canonical
workdir. Reverting one part of the mechanism at a time:

  candidate unresolved (Clean)        5 tests / 15 subtests fail
  one-level fallback                  3 tests /  8 subtests fail
  base side left unresolved           5 tests /  9 subtests fail
  clean before resolve ("..")         2 tests /  5 subtests fail
  helper walks only one level          4 tests /  9 subtests fail

The lint side is covered too: classifyLocalPathTarget uses the same predicate,
and there the same false negative flips direction — a workdir-local path with a
missing intermediate directory matched neither of its signals, so the link was
published instead of blocked.

cmd/multica has no Windows job, so the guard's Windows-only behaviour ran
nowhere: a candidate on another volume, and a directory junction, which
filepath.EvalSymlinks resolves to its own name rather than descending through it
(internal/daemon/config_windows_test.go asserts that refusal directly). A
junction inside the workdir pointing out of it therefore still reads as inside,
before this change and after it. Failing closed on a component that exists but
cannot be canonicalized would reject every path under a pnpm-style node_modules
junction, and on a host whose workdir is itself reached through a junction it
would reject every --content-file and --attachment on that host — a larger break
than the one it prevents. The windows-tagged test pins the gap with the
observations it rests on and fails with instructions if the platform starts
resolving junctions. Its CI step is scoped and runs with -v so a silent skip is
visible rather than passing as "ok".

* fix(cli): lead with the missing file when a rejected path does not exist (MUL-4252)

The workdir containment guard answers one question and reports it as if it
answered two. A path outside the workdir that does not exist got the same error as
a stale file sitting in /tmp:

  --content-file path "/tmp/desc.md" resolves outside the current working
  directory; write agent temp files inside the task workdir ... Pass
  --allow-external-file to override.

The only remedy that error offers is the flag, so the caller's reasonable next
move is to disable the guard and retry — earning a second, unrelated "no such
file or directory". The diagnosis points away from the actual problem, and the
lesson it teaches an agent is to turn the guard off.

MUL-4252 was about reading a file that exists and belongs to another run. When
the path does not exist there is no such file to protect anyone from, so the
missing file leads, with the containment problem kept as the second clause since
the caller still needs to know the flag would not help.

pathExists treats any stat error other than "not exists" as "exists", so a
permission wall or a broken mount keeps the guard's own wording and stays
fail-closed rather than being reported as a missing file.

Both halves are pinned by mutation: dropping the branch fails
TestExternalFileErrorLeadsWithMissingFile on the missing-file assertion, and
making pathExists report everything as missing fails the same test on the
assertion that an existing external file must still be told about
--allow-external-file.

* fix(cli): preserve Windows path kinds and resolve junctions in the containment guard (MUL-4252)

Addresses the two blockers in the review on 3ec1781e:

1. absNoClean prefixed the working directory onto every non-IsAbs path,
   but on Windows a root-relative path (\tmp\desc.md) resolves against
   the volume ROOT and a drive-relative path (C:tmp\desc.md) against the
   current directory on that drive. The guard inspected
   C:\task\workdir\tmp\desc.md while os.ReadFile opened C:\tmp\desc.md,
   so an in-workdir shadow could pass an outside read. absNoClean now
   splits per platform (path_other.go / path_windows.go) and preserves
   the kinds; a drive-relative path on an unobservable drive is handed
   back uncanonicalized and fails closed through the Rel branch.

2. The junction case in workdir_guard_windows_test.go pinned a known
   contract violation (EvalSymlinks refuses to descend through junctions
   under the winsymlink semantics this module ships with). Resolution is
   now component-wise on Windows (evalPath) and follows junctions via
   os.Readlink — which answers under both winsymlink settings — with a
   bounded follow budget; reparse points Readlink cannot speak for
   (OneDrive placeholders) stay opaque. A junction out of the workdir is
   rejected, one into it (the pnpm shape) is still admitted, and the
   known-gap test is replaced by cases asserting both directions plus a
   chain and a dead target.

The shared walk in ResolveSymlinksBestEffort is now component-wise on
both platforms: it applies ".." to the resolved prefix even when the
tail is missing (previously the second phase started from filepath.Abs,
which cleans first), and it never depends on what EvalSymlinks does at
a filesystem root. Unix behaviour is unchanged for every shape the
existing tests cover; the new dot-dot-in-the-walk case pins the
difference.

* fix(cli): classify link targets with the full Windows path grammar and resolve unknown reparse points by handle (MUL-4252)

Addresses the two blockers in the review on d3d370377:

1. Link targets were spliced onto the link's parent unless
   filepath.IsAbs said otherwise, but the kernel applies the full path
   grammar to targets: a root-relative target (\outside) resolves from
   the root of the volume the link sits on, and a volume-GUID target
   (\\?\Volume{GUID}\...) is absolute in the device namespace. The
   resolver now classifies targets through classifyTarget, which reuses
   the same grammar as absNoClean; device-namespace targets without a
   Win32 spelling are kept verbatim so the containment comparison fails
   closed across the namespace instead of splicing a mangled relative
   path (the prefix rules mirror internal/daemon's #6883 handling).

2. An os.Readlink failure on a ModeIrregular reparse point no longer
   means "does not redirect". Readlink decodes only the symlink and
   mount-point tags; other name-surrogates (e.g. container-isolation
   links) redirect through a filter while the lexical location stays
   behind. Such components are now resolved through
   GetFinalPathNameByHandle (zero desired access, so no hydration, and
   FILE_FLAG_BACKUP_SEMANTICS for directories); if the handle names the
   same path the component is opaque, otherwise the walk continues from
   the final path. When even the handle cannot name the location the
   resolver returns the new util.ErrUnresolvablePath instead of
   guessing: fileWithinWorkingDir treats it as outside (fail closed)
   and execenv's delegate falls back to the input, which its
   containment comparison rejects.

ResolveSymlinksBestEffort now returns an error so the fail-closed case
is explicit rather than a look-alike path. Negative tests cover a
root-relative symlink target with an in-workdir shadow (the kernel's
target is verified by content before asserting rejection) and a
volume-GUID junction target with the prefix-stripped shadow.

* fix(cli): classify drive-absolute link targets before the volume check (MUL-4252)

classifyTarget asked VolumeName before IsAbs, but VolumeName is
non-empty for a drive-absolute path (C:\dir) too. A junction's absolute
target was therefore misread as drive-relative and resolved against the
working directory — a nonexistent spelling — which errored the
resolution and left the escape to be judged lexically inside the
workdir. The IsAbs branch is asked first, and the load-bearing ordering
is pinned by TestClassifyTarget, a fixture-free table over the target
grammar that would have caught this without a filesystem.

* ci: run the util link-target classification table on Windows

TestClassifyTarget pins the branch ordering classifyTarget's link-target
grammar depends on, but windows-tagged tests in internal/util had no
execution point: the windows-execenv job only ran the guard tests in
cmd/multica. Scoped to the table by name, matching the job's pattern.

* test(cli): correct classifyTarget expectations for root and base shapes

The walk base is the volume root and the working directory's components
live in the segments for a drive-relative target, and the plain-relative
branch returns the link's directory verbatim without a trailing
separator. The expectations said otherwise; the code was right.

---------

Co-authored-by: weiweiwei <[email protected]>
2026-09-21 16:25:13 +08:00
c375f83c2e MUL-7418: add issue event and time wakeups (#8467)
* feat: add issue event and time wakeups (MUL-7418)

Co-authored-by: multica-agent <[email protected]>

* feat(wakeups): capture issue collaboration event lifecycle

Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): preserve reply source when reopening threads

Co-authored-by: multica-agent <[email protected]>

* feat(wakeups): summarize sidebar and issue activity cues

Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): restore disabled configurations with a two-way toggle

Co-authored-by: multica-agent <[email protected]>

* feat(wakeups): manage workspace wakeups in Autopilot

Co-authored-by: multica-agent <[email protected]>

* chore(wakeups): reserve migrations after current main

Co-authored-by: multica-agent <[email protected]>

* test(wakeups): align inventory fixtures with shared contracts

Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): distinguish trigger conditions and execution results

Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): suppress registration feedback and drain bounded evidence

Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): align sidebar actions with titles

Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): bound database work and unify shared issue visibility

Co-authored-by: multica-agent <[email protected]>

* feat(wakeups): filter mutation events by member or agent actor

Co-authored-by: multica-agent <[email protected]>

* refactor(wakeups): merge structured evidence and narrow receipt locks

Co-authored-by: multica-agent <[email protected]>

* feat(wakeups): let people edit wakeup instructions

Co-authored-by: multica-agent <[email protected]>

* fix(wakeups): align inventory controls and compact prompt editing

Co-authored-by: multica-agent <[email protected]>

* fix(mobile): handle deferred agent run status

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-21 04:17:12 +08:00
8ce795b007 MUL-7480: Steer active agent turns from comments (#8605)
* feat: steer active agent turns from comments (MUL-7480)

Co-authored-by: multica-agent <[email protected]>

* fix: make comment steering hint driven

Co-authored-by: multica-agent <[email protected]>

* fix: harden active comment steering races

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Forge-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-20 19:00:53 +08:00
Bohan Jiangandmultica-agent 9018df3efb feat(projects): set a repository's checkout ref from the UI (MUL-7504) (#8592)
* feat(projects): set a repository's checkout ref from the UI (MUL-7504)

A project that tracks one delivery line could already pin the branch its
tasks start from — `github_repo.resource_ref.ref` has been stored, sent to
the daemon and honored at checkout since #4467. The UI only ever displayed
it. Anyone working through the web, desktop or mobile app had to drop to
`multica project resource update --ref` to set something the interface
already showed as a property of the resource, and until they did, every
task silently started from the remote default branch.

This adds the missing input, on the three paths a repository is configured:

- Create-project modal: a "Branch, tag, or commit" field per selected repo,
  shown once a URL is entered so the common default-branch case still reads
  as one field.
- Resource panel: the same field in the attach form, plus a dialog on each
  attached repo — the row previously offered only delete.
- Mobile: the field in the attach sheet, and the ref rendered in the list,
  which never showed it at all.

Three details the shape of the existing code forced:

- The ref renders on its own line rather than appended to the repo label. A
  custom name took that slot, so naming a repo hid the one signal that its
  tasks do not start from the default branch — and mobile is the only client
  that collects a name.
- Edits spread the stored ref before overwriting. The server replaces
  resource_ref wholesale, so a payload carrying only `ref` is rejected for a
  missing URL.
- Pasting a `.../tree/<branch>` URL now splits into the two fields. Mobile's
  URL check accepted that whole string and stored it as the clone URL, so
  the gap was already producing targets that cannot be cloned.

Server-side, `ref` was stored with no validation beyond a trim, so a typo
survived to checkout and surfaced as a daemon 500 naming its local repo
cache, minutes later and to whoever ran the task rather than whoever typed
it. validateGitRef applies the shape rules from `git check-ref-format` plus
a length cap. Existence on the remote is deliberately still not checked:
the server cannot see the repository, and an offline daemon or a private
repo must not block saving configuration.

Free text rather than a branch picker, by design. Nothing in the product
can list a repository's branches today, and a picker could not express a
tag or a commit SHA anyway. A "list remote branches" endpoint remains the
follow-up #8572 proposes; it should stay an input aid, never the only way.

Co-authored-by: multica-agent <[email protected]>

* feat(projects): make the pinned branch the delivery target too (MUL-7504)

Pinning a repository's starting point only solved half the problem. Nothing
in Multica creates pull requests — `gh pr create` is the agent's own call,
and without `--base` it targets the repository's default branch. So a
project pinned to `release/2026-09` had its agents start in the right place
and then open pull requests into `main`, carrying every commit the release
line had that `main` lacked. Product review asked for the two halves to
move together, so the brief now states both.

The Repositories section names each repo's starting point and, when
anything is pinned, says to deliver back to it. The Project Context bullet
stops implying `--ref` is how you reach the configured revision — the
daemon already applied it, and `--ref` is the override.

The delivery rule is stated conditionally, because a pin is not necessarily
a branch. The field accepts anything git resolves, and neither the server
nor the daemon can tell a branch from a tag or a commit without asking the
remote — which the product deliberately does not do. A tag has nothing to
merge back into, so the agent is told to confirm rather than guess a base.

That same ambiguity shapes how the UI narrows to branches. The field now
says "Starting branch" and explains that tasks both start and deliver
there, but the promise cannot be enforced: `v1.2.3` is a legal branch name
and `main` is a legal tag. Only a full-length object id is unambiguous, and
only that is declined, pointing at `multica repo checkout --ref` where a
one-off revision belongs. The stored grammar is unchanged, so the CLI and
API still accept tags and commits — the daemon has always resolved all
three, and this is a UI-level product choice, not a data restriction.

Two fixes from the same review:

- An unpinned repository now reads "Default branch" instead of showing
  nothing. Clearing a branch used to make the line vanish, which looks
  identical to the setting never having existed — there was no way to
  confirm from the panel that a repo was deliberately on its default, or
  that the row had a branch setting at all.
- The edit dialog says the change applies to newly started work. The ref is
  read when a task is claimed, so work already underway keeps the branch it
  started on, and the copy now says so rather than leaving it to be
  discovered.

Co-authored-by: multica-agent <[email protected]>

* fix(projects): close three gaps in the starting-branch flow (MUL-7504)

All three found in review of the two commits before this one.

**A resumed task could be told to deliver somewhere new.** The starting
point in the brief is the project's CURRENT setting, read when the run was
claimed. A task that started from `release/a`, left uncommitted work and
resumed after the project moved to `release/b` kept its old checkout — the
work is still branched off `release/a` — while the brief now named
`release/b` and told it to deliver there. Following that would push the old
line's work into the new one, and would break what the edit dialog
promises: work already underway keeps the branch it started on.

The brief cannot settle this alone, because whether a checkout gets reused
is only known once `repo checkout` runs. It defers to the checkout instead,
which already reports `Kept` and names the branch it is on.

**Create Project accepted a branch it had already rejected.** The submit
gate only checked the add-a-repo field, so a branch edited on an
already-selected repo reached the payload with nothing but an inline error
to show for it — and the server accepts commit ids by design, so nothing
downstream caught it. Both the button and handleSubmit now check every
selected repo; the button alone was never enough, since TitleEditor's
onSubmit calls handleSubmit directly.

**A second pasted browse URL was stored whole.** Splitting `/tree/<branch>`
was gated on the branch field being empty, so pasting another repository's
browse URL after one was already filled in saved the entire `/tree/...`
string as the clone URL — a target that cannot be cloned. Normalising the
URL is now unconditional in all three entry points. Whether to overwrite
the BRANCH is the separate question, and the pasted pair wins: the branch
field only appears once a URL is present, so a value sitting in it came
from the previous URL rather than from something typed ahead of time.

One defect the review surfaced indirectly: GithubRefField took an optional
`id` and the per-repo instance in the create-project modal passed none,
which detached the label from its input — leaving the field unnamed for a
screen reader, and unreachable by getByLabelText, which is why the gap had
no test. The id is now generated when omitted.

Co-authored-by: multica-agent <[email protected]>

* fix(execenv): stop naming the checkout as the resumed delivery target (MUL-7504)

The resume warning added in the previous commit told the agent to deliver
kept work "to the branch it actually started from, which the checkout
names". The second clause is wrong. A kept checkout reports the branch the
worktree is ON — resolved by `git symbolic-ref`, so the task's own
`agent/<name>/<task>` branch. That is the HEAD of a pull request, never its
base, and nothing in WorktreeResult carries the ref it was cut from. An
agent following that sentence would pass its own branch as `--base` and
open a pull request whose base equals its head.

The warning now says what the reported branch actually is, and points at
the sources that do settle the question: the base of the work's existing
pull request, or the target the task states, and ask when neither does.

It also moves out of the `if pinned` branch. A project cleared back to its
default branch renders no starting point at all, yet a task resumed after
that change still holds a checkout cut from the old one — gating the
warning on a pin dropped it exactly where the mismatch is invisible. The
one sentence that only makes sense alongside a listed starting point
("do not retarget it to a starting point listed above") stays conditional,
so the cleared variant does not point at a line that is not there.

Regressions cover both resume shapes — pinned A to B, and A cleared to the
default branch — plus the specific wrong phrasing, so it cannot come back.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-09-20 17:10:08 +08:00
6b69b3710c Revert "MUL-7517: clarify squad leader identity framing (#8597)" (#8600)
This reverts commit a51fd83072.

Co-authored-by: Forge-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-20 14:57:49 +08:00
a51fd83072 MUL-7517: clarify squad leader identity framing (#8597)
* fix(squads): clarify leader identity framing (MUL-7517)

Co-authored-by: multica-agent <[email protected]>

* fix(squads): address briefing review feedback (MUL-7517)

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Forge-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-20 14:48:17 +08:00
Bohan Jiang d7009df8cc Revert "MUL-7300 fix(issues): preserve Quick Create original input (#8519)" (#8546)
This reverts commit a6472044c3.
2026-09-18 17:46:06 +08:00
a6472044c3 MUL-7300 fix(issues): preserve Quick Create original input (#8519)
* fix(issues): preserve quick-create original input

Co-authored-by: multica-agent <[email protected]>

* fix(issues): validate quick-create origins

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-18 16:17:16 +08:00
Bohan Jiangandmultica-agent 29987fdda5 MUL-7461 test(execenv): clear MULTICA_TASK_CONFIG_ROOT so the suite is environment-independent (#8512)
Four tests in this package failed for every agent running the suite from
inside a Multica task, and passed in CI:

  TestHermesMemoryStorePathLayout
  TestPruneHermesMemoryStores
  TestHermesSessionStorePathLayout
  TestPruneHermesSessionStores

They isolate themselves by pointing HOME at a t.TempDir() and then assert a
path under $HOME/.multica. But HermesMemoryStorePath / HermesSessionStorePath
resolve through cli.ProfileDir, and multicaConfigRoot consults
MULTICA_TASK_CONFIG_ROOT before it ever reaches HOME — returning that root
directly, without the .multica segment. The daemon sets that variable for
every task it runs (taskMulticaEnvironment), so overriding HOME had no effect
and the tests were asserting against a branch they never took. CI never sets
the variable, so it stayed green and the trap only ever hit agents.

Clearing the variable once in TestMain makes the package resolve profile dirs
the same way in both environments. Preferred over a per-test
t.Setenv(cli.TaskConfigRootEnv, "") for three reasons:

  - It also covers TestPruneHermesMemoryStoresDisabled and
    TestPruneHermesSessionStoresDisabled, which passed for the wrong reason:
    their stores were never created under the root the pruner scans, and the
    assertions held only because retention <= 0 disables the pruner outright.
  - t.Setenv cannot be called from a parallel test, so the per-test fix would
    force future tests that resolve a profile dir to choose between running in
    parallel and running in the right environment.
  - New tests in the package are correct by default rather than by remembering
    to copy a line.

A test that wants the task-local branch still sets the variable itself.

Verified on the same commit, whole package:

  go test ./internal/daemon/execenv/ -count=1                        ok
  env -u MULTICA_TASK_CONFIG_ROOT go test ./internal/daemon/execenv/  ok
  go test ./internal/daemon/execenv/ -count=1 -race                  ok
  go vet ./internal/daemon/execenv/                                  ok

Co-authored-by: multica-agent <[email protected]>
2026-09-17 16:33:25 +08:00
Bohan Jiangandmultica-agent d54300abaa refactor(claim): compare only title and description (MUL-7344) (#8488)
Status leaves the compared set, so the snapshot is now just the two body
fields. The comparison answers one question — must the agent re-read the issue
BODY? — and status never helped answer it: its current value ships on every
claim, so the agent learns it without a read.

Worse, comparing it worked against the feature. The workflow has the agent set
in_progress when it starts and in_review when it delivers, so the agent's OWN
bookkeeping came back the next turn as "changed: status" and demanded a body
re-read that nothing in the body justified. Replaying this issue's 21
follow-ups, that alone cut "unchanged" from 17 to 4. The earlier rationale for
keeping it — a free signal that somebody intervened — was wrong on both counts:
it is not free, and the agent can see an intervention by comparing the shipped
status against the value it last set itself.

issueSnapshotVersion stays 1, and this time rows in the old shape DO exist.
That is still correct: removing a field is backward compatible, because every
field the new code reads is present in an old row and still means the same
thing, and the stale "status" key is ignored. The comment now states the actual
bump rule rather than "the set changed" — bump when an old row can no longer
answer the current question, which means adding a field or changing how a value
is derived, not removing one. A test pins the legacy shape comparing correctly
in both the unchanged and the changed direction, so that claim is checked rather
than asserted.

No migration: the column is JSONB and the schema is unchanged.

TestClaimTaskByRuntime_AssigneeAndPriorityAreOutOfScope becomes
NonBodyFieldsAreOutOfScope and now moves status alongside assignee and
priority, asserting the current status still ships with its NEW value — without
that, the fixture's UPDATE could silently not apply and the empty changed-list
would prove nothing.

Co-authored-by: multica-agent <[email protected]>
2026-09-16 17:29:41 +08:00
f39debf8d1 MUL-7344: skip the issue read and the comment scan when the server already answered them (#8377)
* feat(daemon): read the comment delta with one --since, not a roots scan (MUL-7344)

A resumed follow-up turn whose claim carried a server-computed issue-wide
comment delta was still told to run the workflow's two-step scan: a
`--roots-only --summary` pass to discover which threads moved, then a
`--thread ... --since ...` expansion per thread. That rediscovers, in two or
more calls, an answer the server already has.

`--since <anchor>` without `--thread` IS that answer.
ListCommentsSinceForIssue returns every comment on the issue created after the
anchor, in every thread and at every nesting level, with no folding, on the
same `created_at > since` predicate CountNewCommentsSince counted with. It is a
strict superset of what the roots scan could surface: roots-only
`last_activity_at` is MAX(created_at) over a thread, so any comment able to
move a thread's activity is already in the `--since` result.

So BuildNewCommentsHint now hands over exactly one wide read and says it
answers step 2's scan — the same rule BuildResumedCommentsHint already applies
to a server-computed EMPTY delta. The optional second command stays a THREAD
read on `--tail 30` for the reply itself; it is never the wide read, and it
never combines `--thread` with `--since`, which drops the thread root.

Brief step 2 and the multica-platform skill's bounded-reads rule both gain the
matching static sentence, so an agent is not left choosing between the hint and
two surfaces that contradict it.

Cold start, dropped resume and unknown-delta paths are untouched, byte for
byte: only an affirmative server report may stand in for the scan.

Co-authored-by: multica-agent <[email protected]>

* feat(claim): compare the issue against the last run so a resumed turn can skip the read (MUL-7344)

A follow-up turn that resumes its provider session already holds the issue it
read minutes ago, and was still told, unconditionally, to run `multica issue
get` before doing anything. On an issue that did not move, that read returns
bytes the session already has.

The claim now answers the question instead. Each claim records the comparable
state of the issue it built its payload from — status, assignee, priority, and
sha256 of title and description — in a new `agent_task_queue.issue_snapshot`
JSONB column, and compares against the snapshot the SAME agent's previous run
on the SAME issue recorded. The response carries `issue_state_delta_known`,
`issue_changed_fields`, and the current `issue_status` / assignee.

Why a per-claim snapshot and not a `content_revision` on `issue`: a revision
column has to be bumped by every writer that touches one of those five fields,
and the first one missed reports "unchanged" forever with no symptom. A
snapshot taken at claim time has exactly one writer. It also rides the anchor
the comment delta already uses (agent_id, issue_id, started_at DESC), so both
deltas on a claim mean "since your last run" in the same sense.

The daemon renders one of three things, gated on a REAL resume:

  - unchanged  → reported as workflow step 1's answer; the read survives only
                 as "if resumed memory is not enough".
  - changed    → the changed field NAMES plus the read, so the agent knows what
                 to look for instead of diffing blind.
  - otherwise  → the pre-existing instruction, byte for byte.

`issue_state_delta_known` carries the same contract as
`new_comments_delta_known` (MUL-6984), and for the same reason: a cold start,
a missing prior snapshot, a read error, a foreign shape version and an old
server all produce an empty changed-field list, identical to a genuine
"nothing changed". Only the flag separates them, and only the affirmative one
may stand in for the read.

The compared set is named in full in the prompt rather than summarised as "the
issue": labels, parent, due date, stage, project and metadata are NOT compared,
and an agent told "unchanged" has to be able to see that.

The snapshot write is NOT gated on the task being comment-backed — an
assignment run that recorded nothing would leave the next run with no baseline.
It shares the receipt's CAS, so a stale handler cannot write over a newer
reclaim or after execution starts, and a snapshot that fails to encode is
simply not recorded: the next run reports "not compared" and reads the issue.

Co-authored-by: multica-agent <[email protected]>

* fix(claim): renumber the migration past main and fold in review nits (MUL-7344)

CI caught a real collision: main landed 471_comment_deleted_at through
474 while this branch was open, so 471_agent_task_issue_snapshot shared a
numeric prefix and TestMigrationNumericPrefixesAreUnique failed. Renumbered to
475 after merging main; no other migration references it.

Review nits from the PR review, all non-blocking, all cheap to take now:

1. Brief step 2's closing sentence told the agent that a resumed run's scan
   surfaces moved threads via `last_activity_at`. That is now true only on the
   path where the delta is UNKNOWN — when the server hands over a `--since`
   delta there is no scan to read it from. Rewritten to attach the
   `last_activity_at` guidance to the scan itself, wherever the scan happens.
   Done in this PR rather than deferred because this PR already moves brief
   bytes, so it costs no extra prompt-cache invalidation.

2. The two per-claim anchor reads are now one. GetLastTaskStartedAtForIssueAndAgent
   and GetLastTaskIssueSnapshotForIssueAndAgent selected the same row with the
   same WHERE and ORDER, differing only in projection; they are replaced by
   GetLastRunAnchorForIssueAndAgent returning started_at and issue_snapshot
   together. A comment-triggered claim is back to ONE anchor read, as before
   this branch.

   The saved round trip is the smaller half. The real gain is that "since your
   last run" is now a single resolved fact on a claim instead of two
   independently-resolved ones that could disagree. The shared read also shares
   a failure mode, and both consumers already degrade the same safe way: comment
   scan required, issue comparison unknown.

3. The hint's `%d new comment(s)` excludes the injected trigger and the agent's
   own comments; the `--since` read it hands over has no such filter, so the row
   count is legitimately higher. Narrowing the read to match would mean a second
   count-shaped query for no gain, so the text now states the mismatch and its
   direction outright instead of leaving the agent to reconcile two numbers.

New test pins the merge: one claim, one prior run, both deltas resolved from
that single anchor row.

Co-authored-by: multica-agent <[email protected]>

* fix(migrations): renumber the issue-snapshot migration to 478 (MUL-7344)

main landed 475_issue_status_key_not_reserved through 477 while the previous
renumber was in flight, so 475_agent_task_issue_snapshot collided in turn.
Merged main again and moved to 478. TestMigrationNumericPrefixesAreUnique
passes, and the full ledger applies cleanly from scratch through 478.

Co-authored-by: multica-agent <[email protected]>

* fix(claim): measure both deltas from the run whose session is resumed (MUL-7344)

Review found a real correctness bug, not a performance one: the claim could tell
a resumed agent "the issue is unchanged" while the session it handed back
predated the change.

Both deltas were anchored on the most recently STARTED run for (agent, issue).
The session the daemon resumes is chosen by a different rule, and the two are
not the same row:

  - GetLastTaskSession skips poisoned and retired sessions, so a newer run that
    died on an unprocessable conversation is passed over for an OLDER healthy
    one.
  - A manual rerun resumes the exact source task the operator clicked.

So: an old session remembers description A, the user edits it to B, a newer run
records B and then dies unresumable. The next follow-up resumes the OLD session
but compares B against B, reports "unchanged", and the agent keeps working from
a context that still says A. PriorSessionResumeUnavailable does not catch this —
it flags a missing Codex rollout, which is only one of the ways the resumed
session can be older than the newest run.

The fix binds both deltas to the run actually being resumed. GetLastTaskSession
now returns that row's started_at and issue_snapshot alongside its session, the
rerun path takes them from its source task, and the delta computation moves
after resume resolution so it can only ever read the adopted row. Where no
session is adopted — no prior session, a session on another runtime, a
forced-fresh retry, a rerun whose source failure poisoned the conversation — no
delta is reported and the daemon performs the reads it always has.

That makes the standalone anchor query unnecessary: the anchor is now a
projection on a row the claim already fetched, so a claim reads no extra row at
all (GetLastRunAnchorForIssueAndAgent is removed).

The precondition is now visible in the fixtures: a prior run has to be
resumable for a delta to exist, so the comment-delta tests seed a session_id.
The regression test covers both paths and was verified to fail against the
previous anchor — both sub-cases report an empty changed-field list, which is
exactly the wrong "unchanged".

Also renumbers the migration 478 → 491; main landed 478_issue_status_category_expand
through 490 while the branch was open.

Co-authored-by: multica-agent <[email protected]>

* fix(claim): a never-started anchor row may date neither delta (MUL-7344)

Final review found the last corner of the resumed-run bug. Binding the anchor to
the resumed SESSION was necessary but not sufficient: GetLastTaskSession returns
that session's latest TERMINAL row, and that row is not always a run that ran.

A retry child can inherit the session, be claimed — which writes its snapshot —
and then fail before the agent ever starts: runtime offline during prepare,
cancelled while dispatched. Its snapshot describes the issue as of ITS claim,
while the session's memory stops at the last run that actually executed. If the
description was edited in between, the current issue matches that snapshot and
the claim reports "unchanged" to an agent whose context predates the edit.

The started_at gate now covers both deltas rather than only the comment one.
The comment delta was already immune because it read StartedAt directly; the
issue delta read only the snapshot, so it had no such guard. Hoisting the check
makes the row's requirement explicit — a row that never ran dates nothing — and
the inner duplicate is dropped so no reader has to wonder what each guards.

Regression test reproduces the exact fixture and was verified to fail without
the gate: known=true with an empty changed-field list, i.e. the false
"unchanged".

Also from review:

- Adopt the no-adopted-session table (missing session, session on another
  runtime, force-fresh retry, poisoned rerun source). Each seeds a usable
  snapshot AND an unread comment, so a regression has something to report and
  the assertion cannot pass on an empty fixture.
- The resumed-run regression test asserted a comment anchor that never
  executed: both sub-cases created only the trigger comment, which the count
  excludes, so NewCommentsSince was empty and the branch was dead. A comment
  between the two runs now makes it run and pins known/count/since.
- Rewrite the migration header, which still described the old
  "(agent_id, issue_id, started_at DESC)" anchor and an index rationale for a
  column that is never a lookup key.

Co-authored-by: multica-agent <[email protected]>

* refactor(claim): compare title, description and status only (MUL-7344)

The snapshot compared five fields. The comparison answers exactly one question
— must the agent run `issue get` again? — so a field earns a place only if
changing it alters what the agent does AND its current value is not already in
the per-turn message.

Assignee fails the second test: the claim ships the current assignee outright,
and "is this mine now" is answered by that value, not by whether it moved.
Priority fails the first: it is reachable only by reading the issue, but it does
not change what the agent does. Both are dropped.

Status stays even though it is also shipped as a current value, so comparing it
is strictly redundant for "what is it now". It earns its place as a signal
rather than a value: "changed: status" is the clearest evidence that somebody
intervened between the runs — a push back from in_review to todo means the
delivery was rejected — and it costs nothing to carry.

Narrowing the set means a narrower claim, so the prompt now says "compared
title, description and status". An issue whose only change is assignee or
priority is reported as unchanged, which is correct and now pinned by a test
that also asserts the current assignee still ships — without that second
assertion the fixture's UPDATE could silently not apply and the test would pass
on nothing.

Snapshot version goes to 2, because the rule is that the version tracks the
compared SET. No deployment has written a v1 row, but the gate is what makes
narrowing safe in general: a v1 row read by v2 code is "not compared", which
degrades to the unconditional read.

Test fixtures now build snapshots at issueSnapshotVersion rather than a literal
1, so a future bump cannot quietly turn them into not-compared tests while they
keep passing. The foreign-version case stays explicit at +1.

Also renumbers the migration 491 -> 495; main landed 491 through 494 while the
branch was open. Fifth renumber.

Co-authored-by: multica-agent <[email protected]>

* refactor(claim): keep the snapshot at v1 (MUL-7344)

The previous commit bumped issueSnapshotVersion to 2 when it narrowed the
compared set. That was the version rule applied past its purpose.

The rule — "bump when the compared SET changes" — exists to stop a stored
snapshot from being compared against a differently-shaped one. It protects
DEPLOYED rows. This column is introduced by this same unmerged PR: the
migration exists only on this branch, main has neither the column nor the
writer, and no environment has ever stored a row with the wider set. There is
nothing for the bump to protect, and shipping v2 as the first version the
column has ever held would leave a reader hunting for a v1 that never existed.

Back to 1. The `v` field and its unknown-version gate stay exactly as they
were — those are what make a real narrowing safe later. The comment now says
which version this is and why the review-time narrowing did not earn a bump,
so the rule still has a stated precedent without a burned number.

No behaviour change: the compared set is still title, description and status,
and fixtures build at issueSnapshotVersion rather than a literal, so they
followed the constant back down.

Co-authored-by: multica-agent <[email protected]>

* fix(claim): only a completed run may date a delta (MUL-7344)

Final review found a window one step later than the never-started case.

A snapshot is written at CLAIM time. The daemon then writes started_at
(TaskService.StartTask) and only afterwards launches the provider. So a run can
carry both a snapshot and a started_at while its prompt never reached the
session: the daemon exits during prepare, the runtime drops, orphan recovery
marks the row failed. Anchoring there compares the current issue against a
snapshot describing an edit that session never saw — reporting "unchanged"
across a real change, and dating the comment delta late enough to hide every
comment older than that row's start. Reproduced on the real
CreateRetryTask -> claim -> StartTask -> RecoverOrphanedTasksForRuntime ->
follow-up claim path.

Nothing on the row separates "failed after the provider ran" from "failed
before it ran". session_id is inherited by CreateRetryTask at insert, so it does
not mean the session was fed; started_at precedes the launch, so it does not
mean the turn began. The only terminal status that PROVES delivery is
'completed' — a run cannot finish a turn it never started.

So failed and cancelled sources now date neither delta. Their SESSION is still
resumed exactly as before, and GetLastTaskSession still accepts them on purpose;
those runs simply perform the context reads they always did. That is the cheap
side of the trade: a follow-up after a failure or a stop is already recovering
from something, while the case the optimization exists for — an ordinary
follow-up after a healthy run — is untouched.

Both conditions now live in newResumedRunAnchor rather than at the use site, so
"which run may date a delta" is answered once, and GetLastTaskSession carries
the status needed to answer it.

Regression test is the reviewer's repro, verified to fail without the rule:
known=true with an empty changed list AND a computed zero comment count, i.e.
both a false "unchanged" and a false "no new comments".

Also:
- Renumber the migration 495 -> 499; main landed 495 through 498. Sixth
  renumber.
- The migration's content_revision rationale named the old five-field set while
  arguing about hypothetical issue-row writers; it now refers to "any compared
  field", which is what the argument actually depends on.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-16 16:11:16 +08:00
Jiayuan Zhangandmultica-agent 080b118f07 fix(daemon): isolate cached worktree Git identities (MUL-7423) (#8470)
* fix(daemon): isolate Git identities in cached worktrees (MUL-7423)

Co-authored-by: multica-agent <[email protected]>

* fix(repocache): preserve config lock ownership on handoff

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: multica-agent <[email protected]>
2026-09-16 13:37:05 +08:00
Bohan Jiang d665f0ac60 MUL-7379 docs: retranslate the issue status model and correct category-wide behavior claims (#8438)
Documentation half of MUL-7379, after #8435 fixed the code. The stale pages did
not merely use outdated wording — they promised behavior the platform stopped
providing, in the three languages nobody re-read.

zh/ja/ko issues.mdx were left on the retired seven-category inheritance model
when MUL-7240 rewrote the English page. Each still said a new status "inherits
that category's behavior in full", with a seven-row table promising that an
in_review-category status finishes the autopilot run and auto-archives past
failure notices, an in_progress-category status rolls back to todo on failure,
and a backlog-category status parks — plus the worked example naming
`Code Review`. An admin following the Chinese page would create that status and
their autopilot runs would silently never finish. All three now mirror the
English source, including the installed-client wire-enum paragraph they lacked
entirely; ja and ko also gained the agent status-writing paragraph only en and
zh carried. Status keys and category names stay lowercase English per the i18n
glossary, which already listed the four values correctly.

assigning-issues (all four locales) said only the `backlog` CATEGORY parks and
a custom status in it behaves identically. WillEnqueueRun parks on the literal
key, so a reader following that page parks work on their own unstarted status
and gets exactly the run the page promises they will not. The same sentence
named a `cancelled` category, which is now `closed`.

inbox (all four locales) attributed failure-notice archiving to an `in_review`
category that does not exist; it now states the handoff rule #8435 implemented,
including why blocked is excluded.

The Korean settings subtitle said five lifecycle categories directly above the
four groups. CLI_AND_DAEMON.md presented the seven built-ins as the complete
enum while validateIssueStatus is deliberately shape-only.

One behavioral change, in its own commit with its test: the agent brief rendered
each lifecycle group as "- `unstarted`: `backlog`, `todo` (built-in)". That
leading backticked token used to always be a valid status key, so sharing the
formatting was harmless; `unstarted`, `started` and `closed` are now
category-only names that Resolve refuses, so the one line teaching agents how to
write status invited `multica issue status <id> started` — a 400. The category
label is now plain text and only settable keys keep backticks.

Review caught two overclaims, both corrected: filtering resolves on the concrete
status like boards, lists and sorting do, not on the category; and `done` is
both a lifecycle category and a built-in key, so it is not among the names
Resolve refuses.
2026-09-15 15:25:20 +08:00
b62cce56da MUL-7359 perf(runtime): avoid echoing final comment bodies (#8390)
* perf(runtime): avoid echoing final comment bodies

* refactor(daemon): keep the file-first guardrail as the Comment Formatting lede

The receipt-mode rule opened the section, pushing the file-first guardrail
(MUL-2904 / #4182 / MUL-4252) out of the lede. The guardrail is the section's
correctness red line — a shell-mangled body is a wrong comment — while the
receipt mode only decides how much of an already-correct comment is echoed
back. Move the rule after the guardrail in both OS variants; it is now a
single const so the two branches cannot drift.

Co-authored-by: multica-agent <[email protected]>

* docs(skills): drop the duplicated final-comment-receipt section

issues.md states its own charter as "Product contracts the runtime brief does
not fully encode", and SKILL.md gives the brief ownership of "when to
comment". The brief's ## Comment Formatting now carries this rule in full, so
the copy here duplicates a brief-owned instruction against the single-owner
convention (MUL-6984 #7971, MUL-5825 #6517).

It was also unreachable by the documented routing: the section was added to
the file's own table of contents, but SKILL.md's Routing row for issues.md was
not updated, so no task would route here for this topic.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): gate final-comment cleanup on the post succeeding

Agents hand the reply cookbook to one shell call. With cleanup as its own
statement, a failed post is followed by a cleanup that SUCCEEDS, so the call
exits 0 and ./reply.md is deleted. Under `--output table` a failed and a
successful post both write nothing to stdout, which leaves the exit status as
the only machine-checkable signal — masking it lets an agent end its turn
believing an unposted final result was delivered, and drops the body it would
need to retry with. The brief already told agents to check the posting
command's exit status; the rendered snippet made that impossible.

POSIX chains with `&&`. PowerShell 5.1 has no `&&` (it landed in 7), so the
Windows variant propagates $LASTEXITCODE instead. Both keep ./reply.md on
disk when the post fails.

The guard EXECUTES the rendered snippet against a stub CLI: this is a
shell-composition property, and the handler-level test can only prove the
command returns an error, not that the pasted block preserves it. Verified to
fail on the previous two-statement form. The pwsh case runs wherever
PowerShell is installed, including Linux runners, and skips otherwise.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-14 19:11:46 +08:00
d77a1c5ea7 fix(prompts): remove status wire details from operational guidance (#8401)
Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-14 18:16:24 +08:00
aac85ab263 fix(daemon): preserve issue status catalog wire compatibility (#8378)
Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-14 15:52:59 +08:00
686957e743 MUL-7271: test(server): cut slow Go test time further (follow-up to #8295) (#8318)
* test(server): reduce slow unit test waits

Co-authored-by: multica-agent <[email protected]>

* test(server): cut slow Go unit test time further

Follow-up to the first pass on MUL-7271. Test-only changes plus a few
package-private seams whose production values are unchanged.

- Re-executed race-built test binaries no longer pay the race runtime's 1s
  atexit sleep (GORACE=atexit_sleep_ms=0 for child processes only).
- pkg/agent: event-driven or shorter test-only bounds for handshake, drain
  and idle graces; redundant timeout variants merged.
- daemon/execenv: git fixtures copied from a template repo built once; tests
  that re-ran the same worktree lifecycle folded into their supersets;
  ForkLock-guarded fake-CLI writes so parallel tests cannot hit ETXTBSY.
- handler/service: cap and paging tests shrink their caps instead of seeding
  thousands of rows; lock waits probe pg_locks instead of sleeping; one
  shared pgxpool per service test binary; cheaper cron and RSA fixtures.
- wecom: the phishing-link sweep reuses one parser and derives CRLF from LF
  with a sampled cross-check; fake sockets acknowledge instead of waiting out
  the ack timeout.
- migrate: the migration corpus is parsed once; isolated DB tests run in
  parallel.

Co-authored-by: multica-agent <[email protected]>

* test(server): remove transport timeout unit tests

Co-authored-by: multica-agent <[email protected]>

* test(server): restore two non-timeout tests and make a bound load-bearing

Follow-up to the review of #8318. All test-only; no production code changes.

- daemon: restore the three read-liveness cases that the transport-timeout
  sweep took with it. They assert that a ping, a pong and an application
  frame each EXTEND the read deadline, so a healthy wakeup socket survives
  without traffic of our own — the deadline never fires on the happy path,
  which is why they are not transport-timeout tests. Verified they fail
  (all three) when the extension calls are removed from wakeup.go, and pass
  in 0.57s together. The pure "times out without peer traffic" case stays
  deleted.

- pkg/agent: restore the parent-DEADLINE half of the opaque-env redaction
  pair. codex.go treats a parent deadline and a parent cancellation as one
  ending; only the Canceled arm was left covered. The fake now writes the
  secret to stderr before reading the handshake, so the check no longer
  races our own initialize write, and the budget is 2s against a measured
  430-465ms launch. Verified it fails when the !contextEnded guard is
  removed.

- pkg/agent: TestRunCollectQuietDoesNotWaitWhenTheAnswerIsIn had its bound
  widened to a literal 5s, which the shrunk 750ms drain grace can never
  reach — the test could no longer fail. It now raises the drain grace to
  10s for its own duration and asserts under 3s: healthy runs take
  0.19-0.44s, and a broken short-circuit takes 10.19s and fails.

- wecom: gofmt the import block in relay_settle_budget_test.go.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: J <[email protected]>
2026-09-14 14:33:50 +08:00
7dafc0cd62 MUL-7240: Unify issue lifecycle into four stored categories (#8261)
* feat: simplify issue status categories

Co-authored-by: multica-agent <[email protected]>

* feat: adopt four stored issue lifecycle categories

Co-authored-by: multica-agent <[email protected]>

* fix: preserve visible work when upgrading saved category preferences

Co-authored-by: multica-agent <[email protected]>

* docs: align lifecycle comments with stored and wire contracts

Co-authored-by: multica-agent <[email protected]>

* test: align mobile grouping expectations with four categories

Co-authored-by: multica-agent <[email protected]>

* fix(issues): group user views by concrete status keys

Co-authored-by: multica-agent <[email protected]>

* fix(settings): simplify status editor copy and category selector

Co-authored-by: multica-agent <[email protected]>

* feat(statuses): allow ordering built-ins and unify row actions

Co-authored-by: multica-agent <[email protected]>

* feat(statuses): configure custom status icons independently of category

Co-authored-by: multica-agent <[email protected]>

* fix(statuses): dismiss built-in status notice

Co-authored-by: multica-agent <[email protected]>

* fix(issue-status): require moving issues before archive and polish controls

Co-authored-by: multica-agent <[email protected]>

* fix(ci): renumber issue status migrations after main updates

Co-authored-by: multica-agent <[email protected]>

* fix(statuses): isolate archive inspection and align catalog behavior

Co-authored-by: multica-agent <[email protected]>

* fix(statuses): respect injectable catalog for table grouping

Co-authored-by: multica-agent <[email protected]>

* refactor(statuses): simplify built-in skill and runtime guidance

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-14 12:57:24 +08:00
2441638c05 MUL-7284: fix(daemon): keep an existing checkout's work when repo checkout runs again (#8309)
* fix(daemon): keep an existing checkout's work when repo checkout runs again

Re-running `multica repo checkout` on a path that already held a checkout
ran git reset --hard and git clean -fd and moved it to a new branch from the
default branch; isolated checkouts also deleted earlier agent/* branches.
Uncommitted changes, untracked files, and unpushed commits were lost with
output that still read as a normal checkout.

An existing checkout that holds work, or is already on this task's branch,
is now kept exactly as it is and only its remote refs are fetched. The
result reports that it was kept, with its branch and uncommitted/unpushed
counts, and the CLI says so. --fresh restores the old start-over behavior.
Isolated-mode pruning keeps agent/* branches with unpushed commits, and a
linked worktree holding work is no longer removed to migrate it.

MUL-7284

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): carry a migrated linked worktree's unpushed branch into the isolated checkout

--fresh migration of a linked worktree to an isolated checkout removed the
worktree and seeded a new clone whose copied branches are all deleted, so a
branch holding unpushed commits stayed only in the shared cache: out of the
agent's reach and dropped by the next GC. Carry that one branch into the new
checkout (no other cache branch), imported before the task branch so a
same-named task branch moves to a timestamped name.

Align the agent-facing --fresh wording: it discards uncommitted and untracked
files and starts a new branch; commits stay on the old branch, but push any
still needed first, since a linked worktree's old branch lives in the shared
cache that GC prunes.

MUL-7284

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-11 17:02:32 +08:00
mrlonely 6194a8b3a7 MUL-7226 fix(daemon): decline Codex reuse when home preparation fails (#8236)
When the task-local Codex home cannot be prepared on the reuse path, Reuse
now returns nil so the daemon falls back to a fresh Prepare, instead of
returning an environment with an empty CodexHome that fails at launch with
"task CODEX_HOME is missing".

Declining reuse gives up the prior Codex session and the old workdir's
contents; a fresh Prepare that fails the same way still stops the task.

Co-authored-by: mameikagou <[email protected]>
2026-09-10 15:25:14 +08:00
5f1f297d57 MUL-6966 chore(brief): stop teaching agents to use issue metadata (#8102)
* MUL-6966 chore(brief): stop teaching agents to use issue metadata

Phase 1 of retiring the per-issue metadata KV bag: remove every piece of
official guidance that recruits agents into writing it, while leaving the
UI, API, CLI and stored data fully working.

The concept was added when the platform had no custom properties and no
custom statuses. Both exist now, so metadata's stated purpose ("custom
key-value state a future run re-reads") no longer names anything the
product does not model better — what is left is an unstructured bag the
brief actively invites agents to fill, with no defined consumer and no
way for a user to notice it drifting out of sync with status, PR links
and comments.

Removed from the runtime brief:
  - the `## Issue Metadata` section and its dispatch
  - the three `multica issue metadata …` bullets in Available Commands
  - the metadata clauses on workflow steps 1 and 5

Removed from the multica-platform skill:
  - `## Metadata: durable custom state` in references/issues.md
  - the metadata framing of custom properties, replaced by a
    where-state-belongs rule (property / status / comment)
  - the routing-table and legacy-redirect mentions

Kept deliberately: the one negative `pr_url` clause. `multica issue get`
still returns whatever an older run left in the bag, so the warning not
to infer PR state from it is still load-bearing.

Every presence assertion becomes an absence assertion — no test coverage
is dropped. TestInjectRuntimeConfigIssueMetadataSectionScope is inverted
into TestBriefCarriesNoMetadataGuidance, which checks all five task kinds.

Brief size: 15443 → 14264 bytes on a comment-triggered issue run (~315
tokens off every run's always-loaded context).

Phase 2 — removing the UI, CLI and API surface — waits until the
remaining consumers are known to have migrated.

Co-authored-by: multica-agent <[email protected]>

* MUL-6966 chore(skill): drop the pr_url name from the stale-PR-state guard

Review feedback: the surviving `pr_url` mention was not worth its cost.

The guard it sat in is "query the link table, do not infer PR state from
branch names, GitHub search, memory, or something stale" — and that rule
is complete without naming a key. Spelling the key out is itself a way of
teaching the vocabulary: an agent that reads "do not trust the `pr_url`
metadata key" has just learned there is one. Phase 1 is about not putting
that vocabulary in front of agents at all.

The stale-data case is unchanged and still covered: `issue get` returns
whatever an older run left on the issue, and the clause now warns about
that generically.

references/issues.md now contains neither "metadata" nor "pr_url", and
the test asserts exactly that — a blanket vocabulary ban replaces the
five string-by-string checks it had after the previous commit.

Co-authored-by: multica-agent <[email protected]>

* MUL-6966 chore(skill): tell the legacy redirect the truth about metadata

Review nit: the stub promised "Nothing was dropped in the move — the
contracts were reorganized, not shortened." The metadata guidance has now
been dropped, and the daemons that reach this stub are exactly the ones
whose frozen brief still sends them here for it. The one place the claim
is read is the one place it is false.

It now names the exception and states the rule that replaces it —
property for state a person filters by, status for the stage, the result
comment for everything else — so an old daemon's stale pointer lands on
an answer instead of a gap.

This is the one file where saying the word is free: the stub is served
only to pre-merge daemons, whose brief already teaches metadata in full.
Mirroring the note into references/issues.md would put the vocabulary in
front of every new agent forever to serve a window that closes.

Pinned by TestLegacyRedirectsFollowTheDaemonsBrief.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-07 15:32:35 +08:00
2efe1b2a43 MUL-6984 fix(daemon): let workflow step 2 alone decide the comment scan (#7972)
* MUL-6984 fix(daemon): let workflow step 2 alone decide the comment scan

The brief's step 2 called the catch-up scan mandatory while the per-turn
hints in the same context made it conditional ("Need cross-thread
background?", "Only if you need context from the other threads", "If your
reply depends on thread context"). Step 2 now owns the modality; the hints
carry facts and this turn's exact commands and hand the scan over as the
wide read. Hint dispatch keys on PriorSessionID before the delta so a run
whose resume the daemon dropped takes the fresh-session path instead of the
resumed delta hint. The assignment pointer drops the parenthetical that
implied only assignment turns treat the read as mandatory.

Measured on 537 comment-triggered runs: 1 in 10 scans opened a thread the
prompt had not named; 0 of 36 non-scanning runs did; scan median 434 B.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): only waive the comment scan on a delta the server actually computed (MUL-6984)

Addresses review on #7972.

[P1] NewCommentCount == 0 was read as "nothing else was said on this issue",
and used to tell the agent its scan is already answered. The producer in
buildClaimedTaskResponse is best-effort: it writes the count fields only when
the anchor lookup and the count query both succeed AND the count is positive.
A failed anchor read, a failed count query, a cold start with no prior run, a
true zero, and an old server that never sends the fields therefore all arrive
as the same zero — and only the true zero looked at the issue. On a resumed
run a single failed count query was enough to delete the mandatory scan
exactly when the run could least afford it.

The claim now carries new_comments_delta_known, set on the success path
whatever the count. The count fields still stay suppressed at zero (there is
no delta hint to render from a zero), so the flag is the only thing that
separates a checked zero from an unchecked one. Absent on old servers, which
reads as "not authoritative" and keeps the scan — the safe direction in both
mixed-version pairings. The resumed path with no authoritative delta now
renders BuildResumedUnknownDeltaCommentsHint, which keeps the session facts
and hands the scan over as a command instead of claiming it is answered.
Workflow step 2 is tightened to match: only an explicit server report waives
the scan, and silence is not that report.

[P2] The warm path was selected on PriorSessionID != "" alone. MUL-5305 lets
the server withhold a more recent Codex session whose rollout is missing and
return an OLDER fallback session instead: PriorSessionResumeUnavailable is
true while PriorSessionID is non-empty. That older session may resume cleanly,
but it is not the turn this run continues from, so the run was taking the warm
path — and on an empty delta could skip both the full trigger-thread read and
the scan — on the strength of context it does not have. It now requires
!PriorSessionResumeUnavailable and reconstructs from the record instead, which
is what the continuity notice already tells the user happened.

Tests cover the states the existing suite could not see: an older fallback
session with every other warm-path precondition satisfied, resumed-with-delta-
unavailable versus resumed-with-authoritative-zero as siblings, and a
server-side computed zero carrying the flag. Each was verified against the
un-fixed code, where it fails. TestBuildPromptResumedNoDeltaDoesNotForceThread-
Read now sets the flag: its subject is the waiver branch, which is exactly the
branch that requires an authoritative answer.

Co-authored-by: multica-agent <[email protected]>

* docs(handler): correct the new-comment delta's scope on the claim wire (MUL-6984)

Review should-fix on #7972. NewCommentCount's doc comment still described it as
"trigger-thread comments since last run", while the query that produces it
(CountNewCommentsSince: "ACROSS THE WHOLE ISSUE, every thread, not just the
triggering one"), the daemon-side mirror field, and the NewCommentsDeltaKnown
comment directly beneath it all say issue-wide. One struct carrying two
mutually exclusive readings of the same field is worse than either.

Also records on NewCommentsSince that it is suppressed together with the count
at zero, so the next reader does not mistake a non-empty anchor for the signal
that the server looked — NewCommentsDeltaKnown is that signal.

Comment text only.

Co-authored-by: multica-agent <[email protected]>

* fix(daemon): stop calling an older-fallback resume a fresh session (MUL-6984)

Under MUL-5305 the server hands back an OLDER session with the continuity
gap flagged, and the daemon resumes it (ResumeSessionID is not gated on the
flag). The cold hint opened with "This run starts a fresh session on this
issue" and the readable continuity notices said "you are on a fresh one";
both were false in that state, and a test pinned the first.

The cold hint now states nothing about the session — none of the four runs
it serves (first run, dropped resume, explicit fresh, older fallback) makes
a different decision on it, and all four reconstruct from the issue record
with the same two reads. The three readable notices say the run "does not
continue" the lost session and that the memory gone is "from the turns
that did not come back", true in both the fresh and the fallback case.

TestBuildPromptOlderFallbackSessionRequiresReconstruction (renamed) asserts
the reconstruction commands and the notice, and bans "fresh session" and
"fresh one"; the resumed-path negative anchors now key on a phrase only
the cold hint carries.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-04 15:06:00 +08:00
0242f37154 MUL-6986 refactor(skills): merge built-in skills into one platform skill and drop source maps (#7968)
* MUL-6986 refactor(skills): merge built-in skills into one platform skill

Built-in skills are read on the user's machine, which has no checkout of
this repo. Half the shipped payload by bytes was a source-location index:
eight `references/*-source-map.md` files (103KB, 1084 lines) whose content
was `internal/handler/github.go:687` style citations, plus "verification
commands" like `go test ./internal/service` and
`grep -n 'func derivePRState' internal/handler/github.go`. Four SKILL.md
bodies also carried source paths and Go identifiers, and every one of them
pointed the reading agent at its source map ("When in doubt, read that
file"). One source map even leaked an internal branch name.

That was not an oversight: CLAUDE.md required updating a source map in the
same PR as any behavior change, and this test file asserted all seven
existed. The reasoning against it was already written at the top of this
file — anything inside a skill directory is shipped to agent machines, so
evals must stay in _test.go — it just was never applied to the maps.

The nine skills were also split along backend modules, not along how the
work arrives. An agent has one entry point ("I am doing something on the
Multica platform"), so nine descriptions competed in the always-loaded
listing with overlapping boundaries, a cross-domain task activated three
skills, and Quick start / Core model / CLI / Side effects were restated in
most of them.

- Delete all eight source maps.
- Merge the eight platform skills into `multica-platform`: an 81-line
  routing SKILL.md (positioning, the eight shared invariants that were
  duplicated across the old bodies, and a table naming one reference per
  domain) plus `references/{issues,mentions,agents,squads,autopilots,
  projects,runtimes,skill-import}.md`. This is the layout the skill spec
  documents for `references/` — a large skill split by domain, loaded on
  demand — rather than an evidence layer for source.
- Strip every source path, Go identifier, test name, migration ordinal and
  tracker citation from the payload; the behavior contracts themselves are
  unchanged, restated in user-observable terms.
- Scope `multica-onboarding` to Mika. It is one agent's procedure, and
  every agent was paying for its description and its files.
- Add TestBuiltinSkillPayloadCarriesNoSourceReferences so this cannot
  regress, and re-home the ~100 contract anchors onto the reference that
  now owns each one. Anchors match with whitespace collapsed, so a
  paragraph reflow no longer reads as a deleted contract.
- Point the brief's two skill references and CLAUDE.md at the new layout,
  and add one recall line to the brief's Skills section: eight
  descriptions became one, so an agent has one name to guess instead of
  eight. It is emitted only when a listed skill resolves to exactly
  `multica-platform`, so a workspace skill taking that slug silences the
  pointer rather than aiming it at a stranger's skill.

Payload per ordinary agent goes from ~211KB across nine skills to ~100KB
in one; the always-loaded description budget from 1,616 chars to 229.

Co-authored-by: multica-agent <[email protected]>

* MUL-6986 fix(skills): resolve the brief's platform pointer from the built-in

Review found the brief's recall line claimed a fail-closed behavior it did not
have. The claim was that a workspace skill taking the `multica-platform` slug
would silence the pointer; in fact the workspace skill is listed first, wins the
bare slug, and the pointer named it — handing the agent a stranger's skill as
the source of Multica's platform contracts, while the built-in sat at
`multica-platform-multica` and became unreachable.

Documenting that would have left a recall hole, and recall is the one thing this
merge may not cost: eight descriptions became one, so the agent's path to a
platform contract has to stay intact.

Fixed at the source instead. Built-in skills now declare `Source` at load time
(the bundle builder already derived it for the refs path, but the older inline
claim path appended them raw), the daemon carries `Source` into the brief's view
of a skill instead of dropping it, and the pointer resolves the slug from the
built-in's own entry. A collision now points at the built-in wherever it landed.
No bundle hash changes: the bundle builder normalizes `Source` to this value
before hashing.

Also adds TestPlatformSkillDescriptionNamesEveryDomain, which fails if a
reference ships without its trigger word in the description — the merge's recall
guarantee as a test rather than an assertion.

Review nits:
- TOCs for projects.md and runtimes.md, both over the 100-line bar.
- Inlined the two reference-to-reference links in mentions.md, so the routing
  file's "they do not depend on each other" is true.
- Example issue keys use MUL-123, not MUL-2759 — a real internal issue.
- Fixtures naming the deleted skills now name live ones.
- Recorded where the description boundary sits: task nouns are trigger wording,
  body section headings are the inventory the cap rules out.

Co-authored-by: multica-agent <[email protected]>

* MUL-6986 fix(skills): keep the brief's skill pointers resolvable across daemon skew

The runtime brief is assembled by the DAEMON, from a binary the user installs on
their own schedule. Deploying a backend does not rewrite it, and backend
upgrades do not force a daemon upgrade, so renaming the built-ins left two brief
lines — the metadata write discipline and the sub-issue playbook — naming
`multica-working-on-issues`, a skill this server no longer ships. The window is
open-ended, and a pointer to a skill that is not installed is worse than no
pointer: the agent hunts for it and may skip the contract when it misses.

Both skew directions happen, and each needs the fix on the side that is still
deployable:

- Old daemon, new backend. That daemon's brief text is frozen, so the fix has to
  come from the server: a daemon that does not advertise `platform-skill-v1`
  additionally receives a redirect stub under the old name, carrying nothing but
  the new location. A current daemon receives nothing extra, so the stub retires
  itself per install as daemons update — no cleanup release to remember, and no
  cost paid by daemons that never needed it. Gated on capability, never on a
  version string, matching how every other daemon skew is handled here.

- New daemon, old backend. The server has no idea the merge happened, so this
  side has to cope. The brief now resolves both pointers from the skills the
  task actually received: the merged skill if present, else the pre-merge one,
  else no pointer at all. The `--status todo` / `--stage <N>` / secrets rules
  stay unconditional either way — only the "read more here" clause is
  conditional.

Resolution matches on built-in source as well as slug, so a workspace skill that
sanitizes to the same name cannot be advertised as the source of Multica's
platform contracts.

The redirect stub is deliberately a signpost with no contracts of its own: a
stub that grew content would rot against references/issues.md precisely on the
installs that cannot be updated from here, and a test caps its size and forbids
supporting files. It also rides through the template, strict-frontmatter and
source-leak suites like any other shipped payload.

Co-authored-by: multica-agent <[email protected]>

* MUL-6986 fix(skills): route by domain, not by count, and test the redirect wiring

Review must-fix: the routing text turned "load references on demand" into "load
exactly one". `SKILL.md` said to open "the ONE reference that matches your
task", the brief said "open the one reference its routing table names", and a
test pinned the singular.

That is wrong for the tasks the merge was argued on. Creating a squad, assigning
it an issue, then writing a mention needs squads.md, issues.md AND mentions.md;
wording that implies one would have the agent act on contracts it never read —
exactly the recall loss this work is not allowed to cause. Routing is now by
domain: open what the task touches, usually one and sometimes a few, and never
all eight out of uncertainty. The tests pin the domain framing and reject the
singular forms.

Also adds the wiring test the review found missing. The service test passed the
decision in as a bool and the brief test hand-built a skill set, so both ends
were covered while the middle — X-Client-Capabilities on the claim, through the
handler, into the delivered payload — was not. A break there would have left
every existing test green while an old daemon silently lost its redirect.
Covered across both claim branches, since each assembles the built-in set
separately, and verified to fail when the wire is cut. The daemon's capability
header test now pins platform-skill-v1 alongside the worktree gate.

Non-blocking review cleanups:
- The invariant claimed every domain has `list` and `get`; there is no
  `runtime get` and a mention is not a gettable entity. It now points at the
  read-only commands the opened reference actually names.
- Two comments still said the `multica-` prefix means a built-in slug can never
  collide with a workspace skill. This PR ships the fix for that exact
  collision, so they now say what is true: the prefix namespaces, nothing
  reserves it server-side, and the brief resolves pointers from built-in source
  rather than trusting the bare name.
- Recorded the allowed-tools tradeoff: merging eight skills merged their tool
  declarations, so the router declares Bash(git *) / Bash(gh *) for domains that
  do not need them. Dropping them would regress the issue workflow that runs
  `gh pr`; today the daemon's global permission mode means this grants nothing
  the agent did not already have.

Co-authored-by: multica-agent <[email protected]>

* MUL-6986 docs: drop the CLAUDE.md payload rule that a test already enforces

CLAUDE.md is loaded into every task in this repo and asks to stay short:
rules there should be hard to infer from code or easy to get wrong. This one
was neither — TestBuiltinSkillPayloadCarriesNoSourceReferences fails CI on
every class it named, and its failure message already carries the rule and the
reason at the moment someone violates it, which is both cheaper and impossible
to drift from.

The line above it stays: nothing enforces "update the reference in the same
PR" when a CLI flag changes.

Co-authored-by: multica-agent <[email protected]>

* MUL-6986 refactor(skills): accept the skill-name collision instead of handling it

Product decision: a workspace skill whose name sanitizes to exactly
`multica-platform` is rare enough not to design around. Half-handling it was
costing more than the risk — the brief resolved collisions dynamically while the
redirect stub could not (the stub is server-authored text, and the final slug is
only assigned when the daemon writes it to disk), so every review round found
another place that "did not handle it". Either handle it everywhere or nowhere;
this takes nowhere, and states the assumption where the pointers are.

Removed:
- builtinSlug matches the exact name again, instead of built-in source plus a
  slug prefix.
- SkillContextForEnv.Source, the line carrying it through convertSkillsForEnv,
  and the Source stamp on loaded built-ins. All three existed only for the
  collision check, and dropping the stamp also removes a wire-facing change:
  the bundle builder derives "builtin" from an empty id/source exactly as it
  did before.
- The three collision test cases.
- The "the prefix does not guarantee uniqueness, so resolve from source"
  narrative, replaced by the assumption it stands in for: `multica-` is the
  platform namespace, pointers name built-ins by bare name, and nothing
  reserves the prefix server-side yet. When that becomes real the fix is to
  reject the prefix at skill create/import, not to make every pointer
  defensive.

Kept, because it is version skew rather than collision and its window has no
upper bound: the `platform-skill-v1` capability, the redirect stub for daemons
that predate the merge, the brief's new-then-legacy-then-none resolution, and
their tests.

Co-authored-by: multica-agent <[email protected]>

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-04 13:51:12 +08:00
5f0e0f2293 fix(daemon): stop rejecting preparation fields from an older parent (MUL-7029) (#7994)
Every task on an upgraded host failed with

  reuse execution environment: execenv: preparation helper failed:
  exit status 1: decode preparation request: json: unknown field "HandoffNote"

The preparation helper is spawned from the running daemon's executable
path, so parent and helper are the same program but not necessarily the
same build: every upgrade path replaces that file under a live daemon.
Desktop swaps the bundled/managed CLI and defers the restart while the
daemon is busy; brew and manual replacements are picked up by
trySelfReload only on its next idle tick. Until the daemon re-execs, an
older parent talks to a newer helper.

decodePreparationRequest ran DisallowUnknownFields, which turned that
ordinary window into a hard failure of every task on the host. Removing
TaskContextForEnv.HandoffNote (#7626) was enough to trigger it: the field
was untagged and had no omitempty, so installed daemons kept sending
"HandoffNote": "" and the new helper refused the whole request.

The request is now decoded on the same best-effort terms as the response
the parent already reads back — a param the other side does not know
about is dropped, not fatal. Strictness bought nothing here: parent and
helper are both this package, and the shape is pinned by its own tests.

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-03 20:04:35 +08:00