Commit Graph
90 Commits
Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
49098deb6f MUL-6984 refactor(prompt): give each duplicated instruction one owner (#7971)
* refactor(prompt): give each duplicated instruction one owner (MUL-6984)

Four cases where the same data or rule was rendered two or three times into
one context window, following the sibling-block removal in #7937. Each is
resolved by picking the surface that owns it and pointing the others there —
no behavior change beyond what an agent stops reading twice.

The rule used to pick the owner is the one MUL-5377 already set for issue
tasks: the runtime brief holds what is true of this KIND of task (stable, and
it is the prompt-cache prefix), the per-turn user message holds what is true
of this RUN, and the workdir sidecar holds nothing. Autopilot, quick-create
and squad-leader paths had never been cleaned up against it.

Autopilot run data (run id, autopilot id, title, source, trigger payload,
instructions) was rendered in the brief, the per-turn message, and the
sidecar. Now per-turn only; the brief keeps the run-only framing and the
shared issue-command guard. This also settles a contract violation: those are
per-run values, and the brief promises not to carry any. MUL-5696 already
caught the two copies drifting on the guard sentence and fixed that one line
with a shared constant. The payload is now capped at 8 KB with an explicit
truncation notice — it is written by whatever system fired the trigger, so
nothing here bounds it, and a clipped JSON object read as a whole one is
worse than a short one.

The squad leader no_action rule was written four times: the Squad Operating
Protocol, the per-turn prompt, brief workflow step 4, and brief ## Output.
The four had already drifted — only some carried the MUL-6622 escape hatch
that keeps a FAILED `squad activity` call from ending the turn in silence.
The protocol is now the single statement (it is the only document sent
exclusively to leaders); the two brief sites and the per-turn reply imperative
carry the EXCEPTION to their own imperative and point back at it.

Quick-create's five run/output rules were in the brief's Workflow, the
brief's ## Output, and the per-turn "Output format" block; the
--description-file /tmp guidance was in the brief's Available Commands and
per-turn. The brief owns both now — its own comment says these guardrails
must hold "even if the user message is missing" — and per-turn renders only
the field values the modal picked.

.agent_context/issue_context.md is deleted outright. Nothing read it: the
rendered CLAUDE.md never mentioned .agent_context, no per-turn message
pointed at it, and no provider discovers it. Its writer's own comment said
"the runtime brief already carries every fact this file would". With it gone
the directory is created only when the default provider needs
.agent_context/skills, so a local_directory task no longer leaves an empty
managed directory in the user's checkout.

Compatibility: prompt text only. No wire field, CLI flag, or DB column
changes, so no server/daemon version coupling. Tests follow the text to its
new owner rather than being deleted: the protocol's no_action rule is now
pinned in internal/handler, the quick-create rules in the brief's own suite,
and the payload cap has its own coverage including the rune-boundary cut.

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

* fix(prompt): drop the autopilot payload cap, pin the moved invariant (MUL-6984)

Addresses review on #7971.

The 8 KB trigger-payload cap is removed. Its rationale was wrong twice over:
webhook ingress already bounds a body at 256 KiB
(handler.maxWebhookBodyBytes), so the payload was never unbounded from
Multica's side; and because de-duplication made the per-turn message the
payload's ONLY rendering, truncating there dropped task input with no way
back — GET /api/autopilots/{id}/runs/{runId} returns the full payload but no
CLI command exposes it, and `autopilot runs` lists runs without payloads. A
legitimate 9 KB webhook would have silently lost fields the autopilot
instructions might name, and the "JSON is incomplete" notice only prevents
misreading, it does not recover anything. The cap belongs with an agent-facing
read-back command and a threshold taken from real payload sizes, not as a side
effect of removing duplicates. Rendering it once instead of twice is still a
strict improvement over the pre-PR behavior.

TestBriefByteIdenticalAcrossRunsForEveryKind could not see the invariant this
PR actually moves: its autopilot row pins run-1 / ap-1 and every variant
mutates only the generic resume / initiator / connected-app fields, so putting
the title, source, payload or description back in the brief left it green.
TestAutopilotBriefByteIdenticalAcrossRunScopedFields adds a second and third
run that vary all of them — including a different autopilot id — asserts the
brief is byte-identical, and asserts the brief contains none of those values.
That second half matters: byte-identity alone also holds if the values reach
no surface at all. Verified against a deliberately reintroduced field, where
it fails as intended. TestBuildPromptAutopilotRunOnly gains the source and
payload assertions so the per-turn message is pinned as their sole owner.

Contract comments that still described the deleted sidecar are corrected:
agent.MinHandoffCLIVersion (the gate always measured the opening prompt — no
provider read the sidecar), the handoff_note field docs on both sides of the
claim wire, the claim handler's note, TaskContextForEnv.HandoffNote, and
errPathPreExists's namespace list. The Reuse path keeps its issue_context.md
mentions, now labelled as cleanup of manifests written by older builds rather
than of anything this code still writes.

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

* refactor(squad): keep the protocol's ownership note out of the prompt (MUL-6984)

Review nit on #7971. The Squad Operating Protocol ended with a paragraph
saying it is the single statement of the no_action rule and that the rule
changes here — true, and worth writing down, but addressed to whoever
maintains the text. That const is sent to the model on every leader turn, so
the leader paid ~40 tokens per turn for a sentence it cannot act on.

Moved to the const's doc comment, next to the other constraints a maintainer
reads before editing it. The skill side already records the same ownership in
multica-squads/references/squad-source-map.md.

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-03 18:57:35 +08:00
879d0de916 refactor(agent tasks): drop the Active sibling runs prompt block (MUL-6984) (#7937)
Every issue turn carried a claim-time snapshot of the same agent's other
in-flight tasks, scoped only by (agent, workspace) and capped at five. The
scope had no relatedness filter, so the list answered "what else is this
agent running right now", not "what else touches the code you are about to
change".

Measured over one day of runs on this workspace: the block was injected 78
times across 30 runs, listing 96 distinct (current issue, sibling) pairs, and
not one pair shared a PR, a branch, or an issue family. `run-messages` was
invoked zero times off it. Meanwhile the day's one genuine overlap — a PR
review pushing to server/internal/metrics/pricing.go and a concurrent change
to the model catalog next to it — was never paired, because overlapping in
time is not the same as overlapping in code.

The duplicate-run defect this shipped with (#6947) is fixed by
shouldSuppressActiveSelfAssignment, which stops a second run on the same
(issue, agent) at the server. Once that holds, the block can only ever list a
DIFFERENT issue, so it was structurally unable to help with what it was added
for. #7922 then gave the coordination question a correct answer —
`issue runs --active` / `--siblings`, cross-agent, family-scoped, and read at
the moment it is asked — which is the same information as a pull instead of a
per-turn push.

Removed end to end rather than hidden behind a flag: the prompt block, the
claim payload field on both sides of the wire, and ListActiveSiblingIssueTasks.
The field was `omitempty` and the daemon already rendered nothing for an empty
list, so old and new server/daemon pairs stay compatible in both directions;
nothing outside server/ ever read active_sibling_runs.

Kept: the self-assignment suppression and its tests, `--no-start`,
`issue run-messages`, and `issue runs --active/--siblings`.

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-03 13:28:05 +08:00
f93855470f MUL-6874 fix(desktop): recover unexpectedly stopped daemon (#7808)
* MUL-6874 fix(desktop): recover unexpectedly stopped daemon

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

* MUL-6874 fix(desktop): bound daemon recovery retries

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

* MUL-6874 fix(desktop): queue one-shot daemon operations

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

* MUL-6874 fix(desktop): reject nested daemon operations

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-31 15:27:30 +08:00
YYClaw 8c563b49be MUL-6555 feat(issues): preserve source context for sub-issues (#7461)
* feat(issues): preserve source context for sub-issues

* fix(issues): address source context review findings

* fix(migrations): guard source context rollback preflight
2026-08-25 12:00:49 +08:00
2284420966 MUL-6622: fix(server): authorize squad activity by task provenance (#7489)
* MUL-6622: fix(server): authorize squad activity by task provenance

`multica squad activity` rejected every issue that was not itself assigned
to a squad, so a leader woken by an `@squad` mention on an agent-owned issue
or running a leader task bound to a child issue could not record its
evaluation. Because the injected `no_action` rule makes that call the
substitute for a comment, the decision left no trace at all.

The claim path already keys the squad briefing and the mandatory-recording
instruction off the task row (`is_leader_task` + `squad_id`), not off the
issue's assignee. `RecordSquadLeaderEvaluation` now uses the same source:

- load the `X-Task-ID` row first and require it to be a leader task with a
  stamped squad; resolve the squad from `task.squad_id`;
- drop the `issue.assignee_type == "squad"` precondition;
- authorize against `task.agent_id` rather than the live `squad.leader_id`,
  and record `activity_log.actor_id` as `task.agent_id`. A mid-run leader
  change no longer discards a real evaluation, and no longer misattributes
  it in a way that silently desyncs the `no_action` comment suppression
  lookup (which matches on `task.agent_id`);
- name the task's issue in the cross-issue rejection, since a stage-barrier
  wake runs on the parent and reaching for the child id is the common slip.

Leader agents running a NON-leader task are now rejected: they are not
running as the leader, and the runtime only mandates the call for leader
tasks. This is the one behavior narrowing, covered by a test.

The injected instructions also gain a failure fallback. The comment
prohibition is already conditional on the activity existing
(comment.go:1851), so a leader whose recording call errored may comment —
it just was not told to. It now is, closing the silent-drop path for any
remaining failure cause.

Fixes #7487

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

* MUL-6622: fix(server): close review findings on squad activity authorization

Addresses the four items from PR review.

1. Tenant isolation. `GetAgentTask` is a global lookup by id and the
   cross-issue rejection quotes the task's issue id, so an unrelated task
   probed through a readable issue echoed back a foreign workspace's issue
   id. The task is now loaded with `GetAgentTaskInWorkspace`, and check
   order is explicit: tenant scope, then "caller owns this task", and only
   then any message naming a task-derived id.

2. Claim-time role divergence. `is_leader_task` on the row is enqueue-time
   intent: when the leader is swapped before the claim, the claim path
   clears `resp.IsLeaderTask` and runs the task as an ordinary agent turn
   while the row keeps the flag set. Trusting the row alone let such a
   downgraded run write a leader verdict and suppress its own comment, so
   the live `squad.leader_id == caller` gate is restored on top of the task
   ownership gate. `activity_log.actor_id` stays `task.agent_id`, which is
   what the no_action suppression lookup matches on. Removing the live gate
   later requires persisting the role the claim actually delivered.

3. Failure fallback vs. one comment per turn. The new "comment if the
   recording call fails" instruction contradicted the exactly-one-comment
   rule on the `action` path, where a delegation comment already exists. All
   four injection sites now scope the fallback to turns that have not
   commented yet and cap it at one comment. Pinned by
   TestSquadOperatingProtocolRecordsAgainstTheTurnsIssue and
   TestBuildPromptSquadLeaderNoActionFailureFallback.

4. Shared test fixtures. The new handler tests are rebuilt on
   `internal/testutil` (`dbfx.Issue` / `dbfx.Task` / `dbfx.Squad`,
   `testutil.Call(...).Want(...)`) instead of open-coded
   `INSERT ... RETURNING` and recorder quartets, per CLAUDE.md. The shared
   `runningSquadLeaderTaskFixture` moved over too.

New negative coverage: foreign-workspace task and foreign-agent rejections
both assert the response body does not contain the other issue's id, and
a leader change before recording is now asserted to be refused.

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-24 14:38:07 +08:00
8f9c766e40 MUL-6460: feat(brief): inject the workspace status catalog into the agent brief (#7306)
Agents could not see custom issue statuses (MUL-6243): the brief's status
command line hardcoded the seven built-in keys, so a workspace's Human
Review / Rework statuses were invisible even when instructions asked for
them, and the CLI offers no catalog read an agent could fall back on.

Ship the catalog on the claim payload, the same path WorkspaceContext
already rides: buildClaimedTaskResponse reads the workspace's ACTIVE
CUSTOM statuses (built-ins stay off the wire — their keys, names and
semantics are locked) and the daemon copies them into TaskContextForEnv.
The brief's status bullet then renders the workspace's statuses grouped
by category, with each custom status's name and description — category
anchors the inherited platform behavior, name is what users call the
status in instructions, and description is the admin's disambiguator
when a category holds more than one status. A bridge bullet in the issue
workflow connects the existing category-level status rules (which stay
untouched) to the concrete key choice.

With no custom statuses — every old-server pairing included — both
renders are byte-identical to before, keeping the prompt-cache prefix
stable for existing deployments (MUL-5377). The catalog is durable
workspace configuration like WorkspaceContext, not per-turn state, so it
belongs in the brief rather than the per-turn prompt. The claim read is
not gated on the custom_issue_statuses flag: the flag only guards
creation, and a catalog written before a flag flip must stay visible as
long as issues can sit on it. Name/description are user-authored and
pass through sanitizeNameForBriefMarkdown; a key failing the code-token
guard drops its entry. A 30-entry cap with an explicit omitted count
keeps a scripted catalog from flooding the brief while disclosing the
truncation.

Also update the built-in skills that document status semantics
(multica-working-on-issues, multica-squads) and their source maps:
statuses beyond the built-ins inherit their category's behavior in full,
with the two literal-key exceptions (failed-task rollback writes todo,
merged close-intent PR writes done) called out.

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-21 17:40:27 +08:00
55c7cf0575 fix(daemon): add the reference fact to the Mentions brief — @-as-attribution is not a mention need (MUL-6528) (#7379)
MUL-6417 replaced the prescriptive "Default: NO mention" rule with facts
that invalidate each false prior from human @-culture, but only covered
two of the three: notify-for-visibility and courtesy/thank-you. The
third prior — the @-form used merely to write someone's name — was left
standing, and an agent attributing a product decision to "@Steve Jobs"
in prose enqueued a run for the agent it was crediting.

Add the missing fact in the section's established style: crediting or
citing someone is prose about them, not work for them; the link form
dispatches whoever it names, so a reference stays plain text. Pin it in
TestInjectRuntimeConfigMentionLoopHardening alongside the other facts.

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-21 13:41:21 +08:00
3268d01172 fix(daemon): move the in_progress start write into workflow step 3 (MUL-6417 follow-up 2) (#7295)
* fix(daemon): move the in_progress start write into workflow step 3; define ancillary by output (MUL-6417 follow-up 2)

Field evidence from MUL-6460: a run that verifiably received the #7283
brief (its workdir CLAUDE.md carries the two-moment rule) walked steps
1→2→3 for over an hour and never issued a single status write. Two
compounding causes, both in the prompt:

- Placement. The start-moment rule lived in the status block BELOW the
  numbered steps. At the moment its condition triggers the model is
  executing the list, and a rule outside the list does not fire — the
  pre-MUL-6417 opening write was reliable precisely because it was an
  explicit step. The conditional write now opens step 3 itself, at the
  read→work boundary.
- The research example. "A todo issue you were asked to research stays
  todo" was written for consult pull-ins, but the MUL-6460 turn's work
  WAS research toward its own issue's ask — the example pattern-matched
  the exact case it was never meant to cover. Ancillary is now defined
  by output (the turn produced none of the issue's own deliverable),
  and research/design explicitly count as work when they are the ask.

Guard tests pin the step-3 placement and the research-counts-as-work
clause, and ban the retired example. Skill, source maps, and user docs
updated to match.

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

* refactor(daemon): state the status rule as continuous, anchored at the moments that fire (MUL-6417)

Bohan's review of #7295: 'at the end of your work, write the state it
reached' still read as restricting writes to fixed checkpoints. The lead
now states the invariant — keep status true at every point in the turn,
write whenever the work changes it, mid-turn included (a blocker is
recorded when hit) — and the end of the turn becomes a verification
('do not exit with a stale value') instead of the write moment.

The anchors stay. A free-floating 'write when necessary' with no
attachment points is exactly the shape that produced zero writes in the
MUL-6460 incident: permission without a trigger moment does not fire.
Step 3 keeps the start write; the exit check keeps the guarantee that a
turn never ends with a lying board.

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

* fix(daemon): drop activity-word skip-lists; ancillary is decided by output alone (MUL-6417)

Elon's #7295 review: 'review' still sat in step 3's skip clause and the
no-write bullet's example list, replaying the research incident's exact
failure class on an issue whose ask IS reviewing a PR. Activity words
carry no deliverable information in either direction, so both lists are
gone: step 3 names forms of work (code, research, a design, a plan, or a
review when reviewing is the ask), the no-write case is stated purely by
output (the turn produced none of THIS issue's deliverable), and the
rule now says so explicitly — the kind of activity never decides, only
whether the output is part of what the issue asks for.

Guards pin the review-as-work clause and the never-decides sentence, and
ban both retired skip-lists. Skill, source map, and docs synced.

Measured brief delta vs main (claude, production-length UUID): ordinary
13,362 → 13,971; leader 14,467 → 15,076 (+609 each, cached prefix);
per-turn messages unchanged.

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

* refactor(daemon): adopt J's review — one criterion, anchors in steps, category-scoped skip (MUL-6417)

J's review of #7295, all four points taken:

- The positive form-list that replaced the banned skip-list was the same
  failure shape read as exhaustive (a triage or reproduce-the-bug turn
  finds itself in no list). Both directions of activity-word lists are
  gone; the never-decides sentence moves INTO step 3 — the position that
  fires — instead of the status block, the position this PR itself
  proved dead.
- The exit-side check is anchored inside step 5 ('Before exiting, ...'),
  making the PR's own placement argument self-consistent.
- The step-3 skip is scoped to the in_progress CATEGORY, so a custom
  status like Planning already counts as recorded once MUL-6460 puts the
  status catalog in front of agents — no rewording needed then.
- Tense: 'if any part of what this turn will produce ...' — step 3 is a
  prediction; mispredictions self-heal under the continuous rule.

Plus J's meta-point on guards: presence-pins cannot tell where a
sentence lives, and both field incidents were correct sentences in dead
positions — the anchors are now pinned positionally (must appear inside
the step-3 / step-5 lines), and the retired positive list joins the
banned set. Kept the questions/discussion/acknowledgements clause (J's
conservative option): acknowledgements are not covered by 'answered a
question' and the sentence is the MUL-6300-era invariant other tests
pin.

Measured (claude, production-length UUID): ordinary brief 13,685,
leader 14,790 — +243 vs main, down from +609 at the previous commit.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-20 16:09:32 +08:00
4ff37edf91 fix(daemon): record in_progress when work on the issue's ask starts, not only at turn end (MUL-6417 follow-up) (#7283)
Post-merge report from Bohan: a todo issue assigned to an agent sat in
todo for the whole first work turn — half an hour of active work the
board attributed to nobody. The end-of-turn-only rule conflated when the
agent can JUDGE with when the FACT changes: a turn that advances the
issue's own ask makes "being worked" true the moment it begins. The
activity indicator shows the run, but columns, filters, and sorting read
status — the indicator alone is not the surface people watch.

The status rule now has two write moments, same fact anchor:

- start: the moment the turn is known to advance the issue's own ask →
  in_progress (only if it differs — rework on an in_review issue flips
  it back the same way);
- end: the state the work reached (in_review / in_progress-continuing /
  blocked).

An ancillary turn (research, answers, review, discussion) still writes
nothing at either moment, so the research-stays-todo case and the
concurrent-convergence property from the original MUL-6417 change are
unchanged. This is the pre-MUL-6417 timing with the fact anchor's
conditionality — not a revert to the unconditional lifecycle flip.

Docs, skill reference, and source maps updated to the two-moment rule;
guard tests pin the start moment and ban the end-only phrasing.

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-20 14:17:18 +08:00
4f56785b02 refactor(daemon): merge Reply/Ownership turn modes into one fact-driven workflow (MUL-6417) (#7245)
* refactor(daemon): merge Reply/Ownership turn modes into one fact-driven workflow (MUL-6417)

The Reply/Ownership split dated from PR #205 and had already been merged
in substance by MUL-5377 (one brief section + mode router) and MUL-6300
(reply turns own the same status arc). What remained was a per-turn
marker plus two mode blocks restating information the per-turn message
already carries as data. This finishes the merge:

- Status is judged once, at the END of the turn, from what the work
  actually changed about the issue — not from the trigger type, not from
  the run lifecycle, and not gated on being the assignee. No opening
  in_progress write: the board's in-flight signal is the issue activity
  indicator, and a todo issue the agent was only asked to research now
  correctly stays todo (the old unconditional arc got that wrong twice).
  Lifecycle writes oscillate under concurrent runs; fact writes converge,
  which is what the retired assignee gate existed to protect.
- Delivery routes on data: a triggering comment in the per-turn message
  (reply in that thread with THIS turn's --parent, one reply per thread)
  or none (post a new top-level comment).
- The blocked rule and the squad-leader dispatch case become end states
  of the same judgment instead of mode-block special cases; the leader
  keeps one bullet (dispatching members is not delivery). The reply-mode
  mention pointer is gone — the discipline lives in ## Mentions.
- The Turn mode markers are removed from the per-turn prompts, deleting
  the marker/router-drift failure class (1ea3cf1d7) outright.

squad_briefing.go's Squad Operating Protocol text is deliberately
untouched: it does not reference the retired modes, and its guest-leader
prohibition is a squad-protocol product decision, not workflow routing.

Guard tests now pin the fact anchor: conversational turns write nothing,
research on a todo issue leaves it todo, assigned deliverables land in
in_review (stage barriers and parent notifications key off that write),
and concurrent agents converge instead of flapping the board.

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

* test(handler): align squad parent-status contract test with the fact-judgment brief (MUL-6417)

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

* refactor(daemon): replace the prescriptive mention default with the facts behind it (MUL-6417)

"Default: NO mention" told agents what to do without why. The mention
syntax hides its semantics — it reads like a free social gesture but is
a spawn/notify operation — and every real incident was an agent acting
on a false need: "they should know" (comments already reach followers;
completion wakes are platform-owned) or courtesy (a thank-you run whose
only reply is another thank-you run). The section now states those facts
plus the cost asymmetry (a missed mention is one follow-up ask; a stray
one is a paid run), from which the old default is derivable.

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

* fix(daemon): scope the mention notify-fact to followers; sync docs to the new status contract; compress (MUL-6417)

Review fixes from #7245 (Elon):

- "A mention routes work — it is never how someone finds out" was false
  for non-followers — a mention is exactly how a human who does not
  follow the issue finds out, and the absolute wording could suppress
  necessary escalation. The notify-fact is now scoped to followers, and
  a guard pin bans the overreach alongside the old prescription.
- Every surface that still taught the old status contract is synced to
  end-of-turn fact judgment: issues.mdx/zh (agent flips in_progress on
  start), assigning-issues.mdx/zh (todo → in_progress → in_review as the
  default arc), and working-on-issues-source-map.md:177 (reply turns
  gated on assignee — the exact gate this PR removes).

Also a compression pass over the PR-touched brief text (−474 bytes):
drop the intro signpost sentence (steps and status block are
self-contained), tighten the delivery routing and delivery bullet, and
compress the concurrency rationale to one clause. Ordinary brief is now
13,181 bytes vs 13,345 on main (−164), leader 14,286 vs 15,131 (−845);
with the per-turn marker deletion (−139/−142 per turn) every prompt
surface is a net reduction from the first turn.

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

* docs(squads): retire the grant-routing ledger and status-first dispatch ordering (MUL-6417)

Review nit from #7245 (Elon): squad_briefing.go's design comment still
described the reply-mode grant routing and assignee-scoped arc this PR
removed. It now records the current mechanics — the workflow judges
status at end of turn, the leader's brief-side special case is dispatch
leaves the parent in_progress, and this section's job is the
owning/guest permission boundary at the Agent Identity layer. squads.mdx
(en/zh) stops describing a status-write-then-dispatch ordering: the
status point is now an end-of-dispatch-turn fact, consistent with the
assigning-issues docs.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-20 11:32:17 +08:00
212ecfb8dd MUL-6300: fix(daemon): let reply turns own the status arc for their own issue (#7083)
* fix(daemon): let reply turns own the status arc for their own issue (MUL-6300)

A comment-triggered run was forbidden from touching issue status: "Do NOT
change the issue status unless the comment explicitly asks for it", written
in PR #205 (2026-03-31) on the day on_comment became the default trigger. It
was aimed at conversation — questions, thanks, agent-to-agent chatter should
not churn the board — but it also covered the most common real trigger, a
comment asking for rework on a delivered issue. The agent reworked the issue
with its status frozen, and a human had to move the card by hand (MUL-5165).

Reply mode now owns the same arc Ownership mode does — in_progress when the
work starts, the status the work is actually in when the turn completes — but
only for turns that carry work on an issue assigned to this agent. The two
properties the old prohibition really protected are kept and pinned by tests:

- a purely conversational turn never writes status;
- a run that is a guest on someone else's issue (@mention pull-in) never
  writes status, so an agent pulled in to answer a question cannot push
  another assignee's in-flight issue to in_review.

Squad leaders keep routing on the Squad Operating Protocol's "Own the parent
issue status" grant, which the server injects only when the issue is assigned
to that squad — so the guest-leader path (MUL-3724) stays closed, and the
grant no longer has to be phrased as an exception to a prohibition.

Also refreshes the two built-in skills that documented the old rule.

Supersedes #5781, whose one-line exception was written against brief text that
MUL-5377 has since rewritten.

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

* fix(daemon): compress reply-mode status rule, cover unassigned issues (MUL-6300)

Review round on #7083. The ordinary reply-mode status bullet drops from
103 to 70 words and the squad-leader bullet from 110 to 92, with the
conversational-turn sentence now identical in both branches.

The no-write invariant broadens from 'assigned to someone else via
@mention' to 'not assigned to you': an @mention can also land an agent
on an UNASSIGNED issue (triage), which the old blanket prohibition
covered but the first draft of the positive rule left undefined.

Guard-test pins and the fallback pin in reply_instructions_test.go are
synced to the new wording; the arc-trigger substring is unchanged so its
two pins survive as-is. SKILL.md carries the same broadened phrasing.

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

* docs(daemon): purge stale no-status-unless-asked semantics from ledger surfaces (MUL-6300)

Elon's review round on #7083: four surfaces still explained current
behavior with the pre-MUL-6300 rule. The squads source map (agent-
readable) now says the status-ownership grant, not the system comment's
ask, authorizes the leader's wrap-up write; prompt.go's turn-mode
comment, squad_briefing.go's grant rationale (which also still pointed
at the long-gone writeWorkflowComment), and the child-done stage test
comment now describe the conditional arc. Past-tense history quotes
(contract-test design note, MUL-6300 replacement note) stay verbatim.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-18 02:38:18 +08:00
8b1acfd19e fix(execenv): fold redundant metadata-list read into the issue-get step (#7016) (#7042)
Issue-triggered runs paid two reads for the same data: the runtime brief
mandated 'issue get' (whose JSON always carries the metadata bag) and then
a separate 'issue metadata list'. Fold the read guidance into step 1,
renumber shared steps 1-6 to 1-5, and update the brief tests to pin the
new wording and ban the retired standalone read step.

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-17 00:16:41 +08:00
Xichang(Seacen) Zhao cc2aea3878 MUL-6104: fix(daemon): tell the brief's reader not to merge stderr into parsed output (#6870)
Both brief builders now state that `--output json` writes JSON to stdout while confirmations and warnings go to stderr, and that merging them (`2>&1`) into anything that parses the output makes a successful write look failed and invites a duplicate retry.

Adds a regression test anchored on the rule's wording — premise, prohibition and consequence — asserted exactly once per rendered brief, so a reversed or duplicated rule fails the test.

234 bytes per rendered brief; a run renders one of the two builders.
2026-08-14 18:39:13 +08:00
19155e41f9 refactor(prompts): compress duplicate --no-start guidance from #6956 (MUL-6168) (#6971)
PR #6956 stated the same rule — use --no-start when the write only records
ownership/status of work already underway — five times across the always-
injected brief, the sibling-runs block, and the working-on-issues skill.
State it once per surface and keep the flag in the command signatures.
Prompt-text only; no behavior change.

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-14 17:52:04 +08:00
b5a48eca13 fix(agent tasks): prevent duplicate self-assignment work (MUL-6168) (#6956)
* fix(agent tasks): avoid duplicate self-assignment runs

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

* fix(agent tasks): address duplicate-run review

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

* fix(agent tasks): resolve sibling-run nits

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-14 13:57:09 +08:00
8d292ef7ce MUL-5963: feat(server): let web chat read back history via multica chat history (#6680)
* feat(server): let web chat read back history via `multica chat history`

When an agent's provider session resume fails on a web chat / Feishu task, the
daemon injected SessionContinuityNoticeUnrecoverable, making the agent reply
"this is a new session, no memory". But the conversation was never lost — it is
persisted in chat_message, and the agent just had no command to read it back
(only Slack had a history reader).

Make `multica chat history` serve the stored chat_message transcript for any
non-Slack session (web chat, Feishu), reusing the existing task-token
authorization and the shared response shape. Route web chat / Feishu continuity
notices to a new variant that tells the agent to read the transcript back
instead of scripting a "new session" disclosure, and teach the Feishu channel
prompt the read-back command.

- handler/chat_history.go: GetChatChannelHistory falls back to chatMessageHistory
  (ListChatMessages, oldest-first, default recent 30) on ErrNoSlackSession.
- execenv: add SessionContinuityNoticeChatTranscript; keep Unrecoverable for
  channels Multica stores no transcript for (e.g. WeCom).
- prompt.go: route web chat / Feishu to the transcript notice; Feishu channel
  awareness teaches `multica chat history`.
- tests: update surface/notice matrix, Feishu prompt policy, add handler
  transcript fallback tests.

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

* test(server): fix transcript read-back test to insert a real assistant row

The insertChatVisibilityMessage helper hardcodes role='user', so the
"assistant reply" row in TestGetChatHistory_NoSlackBindingReadsStoredTranscript
was actually stored as a user message and the handler (correctly) returned
role "user". Add an insertChatMessageRole helper and use it for the assistant
row so the role-mapping assertion is meaningful.

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

* refactor(server): treat WeCom as a readable transcript surface for session continuity

WeCom is fully wired on main: it persists the conversation to chat_message and
stamps ChatChannelType="wecom", so `multica chat history` reads it back just
like Feishu's. Stop routing it to the Unrecoverable fallback; give it the
transcript notice and the `multica chat history` history command, symmetric
with Feishu (no thread reader, no upload).

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

* fix(server): make transcript read-back reachable and honor the paging contract

Addresses review on the web-chat history read-back feature.

- GetChatChannelHistory no longer dead-ends on "no channel integration" when
  SlackHistory is nil: a no-Slack deployment (the exact target of this feature)
  now serves the stored chat_message transcript directly, and Slack-backed
  sessions still go through the Slack reader.
- chatMessageHistory swaps ListChatMessages for ListChatMessagesPage, honoring
  the ?before cursor (encoded as created_at|id) and clamping ?limit (default 30,
  max 50), and advertises NextCursor on a full page so an agent can page back
  without re-reading the recent window. Author now uses the channel vocabulary
  (Bot / User) instead of the raw role string.
- Adds a single SurfacePersistsTranscript predicate (web chat, Feishu, WeCom,
  DingTalk) as the one source of truth for "which surfaces are readable", shared
  by the continuity-notice router, the chat-prompt history copy, and the surface
  list — closing the DingTalk gap where the notice said "read it back" but the
  prompt said "no history reader for dingtalk".

Tests: nil-reader-with-rows regression, transcript paging without duplicates or
gaps, Author/TS pins, and DingTalk cases in the surface/notice and prompt tables.

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

* test(server): fix transcript paging test assertion

The paging test wrongly asserted messages are strictly oldest-first ACROSS
pages, but paging to older messages means each later page is older than the
previous one — the sequence is not monotonic. The handler was correct; the
assertion was wrong. Now it checks the real invariants: each page is
oldest-first, and the union of all pages is exactly the transcript with no
duplicates or gaps.

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

* fix(server): stop telling transcript surfaces their history is not in Multica

The channel-awareness block opened with "This conversation and its history
live in <platform>, NOT in Multica" for every channel type, and the very
next line told a Feishu/WeCom/DingTalk agent that Multica stores the
conversation and hands it `multica chat history` to read it back. An agent
that believes the first sentence has no reason to run the command in the
second.

Only the useful half — do not go hunting through issues and comments — is
unconditional now. Where the conversation actually lives moved into each
branch: Slack keeps the original wording (true there), transcript surfaces
say the conversation happens on the platform and Multica stores a
transcript of it, and the defensive fallback keeps both claims for a
surface that really has no reader.

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

* fix(server): name the channel a served transcript came from

HistoryPage.ChannelType is documented as empty ONLY for a session bound to
no channel, but the transcript branch never set it. A WeCom/Feishu/DingTalk
session that HAD messages therefore reported channel_type "" — telling the
agent it was in a web-only chat — while the same session's empty read
already named the platform. One endpoint, two answers, decided by whether
the session happened to have messages.

Reuses the existing sessionChannelType helper, so the transcript path
inherits its refusal to guess: a binding read that fails for any reason
other than "no such row" is reported rather than silently answered as
web-only. That failure now returns the same status as the empty-read path
does for the same cause, instead of the 502 reserved for a failed channel
read.

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

---------

Co-authored-by: qiushihao279-cloud <[email protected]>
Co-authored-by: Claude Fable 5 <[email protected]>
Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-12 15:36:39 +08:00
Xichang(Seacen) Zhao 90eadd940a MUL-5905: feat(wecom): send the files an agent produced into the chat (#6604)
* feat(wecom): send the files an agent produced into the chat

An agent that produced a file and answered in WeCom delivered only the
sentence. `multica attachment upload` had already run, the object was in
storage, and CompleteTask had bound the row to the assistant message — but
nothing downstream of that bind knew how to reach a WeCom conversation, so
the file stopped there. The user read "here is the forecast" and got no
forecast, with no error anywhere to explain it.

Worse when the agent said nothing at all: an empty completion with a file
bound to it returned early, so the artifact was thrown away silently.

This adds the last hop. media_upload.go carries a file up the long
connection — WeCom has no REST endpoint for this, so it goes as
aibot_upload_media_init / _chunk / _finish over the same socket, and all
three read their verdict through the request() layer #6590 generalized.
outbound_media.go is the caller: after the answer is out, it reads the bound
attachment out of object storage, decides what to call it, and pushes it in
as its own message.

Three decisions worth naming.

The answer goes first and the file never blocks it. An upload is megabytes
and round trips and it can fail; delivery runs after the words are out, on
its own goroutine and a budget of its own, and its worst outcome is one
extra line saying a file did not make it.

A lost ack and a refusal are opposite failures. A chunk whose verdict never
came is offered once more — the protocol makes re-sending idempotent, and
losing one slice of a hundred must not cost the file. A chunk the server
refused is not, because the refusal is its answer and will be its answer
again. The push of the finished media follows the same rule in reverse: a
timeout is reported as-is rather than retried, because the same media_id
sent twice puts the file on the user's screen twice with nothing to undo.

WeCom validates bytes against the msgtype, so what to call a file is a
decision. A kind past its ceiling is demoted to a plain file rather than
sent and refused — a card the user can open beats a photo the server
rejected.

WithAttachments is what turns this on, and router.go passes it only when the
deployment configured object storage. Without it the adapter behaves exactly
as it did before.

* fix(daemon): stop telling a WeCom agent its conversation is text-only

The per-turn chat prompt and the runtime brief both decided file delivery by
asking "is there a channel at all". Any non-empty ChatChannelType got the
text-only paragraph: `attachment upload` does not apply here, describe the
file in words, never write its local path. That was true of every IM channel
when it was written, and it stopped being true for WeCom, whose adapter now
reads the bound attachment out of object storage and sends it into the room.

So an agent on WeCom was instructed not to use the one command that would
have delivered its file. The capability shipped and the agent was told it
did not exist.

The delivery question is per-adapter, not "is there a channel", and
ChannelCarriesFiles is where that answer now lives. Both call sites branch
three ways instead of two.

The new copy is deliberately built from main's existing web-chat branch
rather than written fresh. Both open on the same clause — "To include a file
or image you produced in your reply, run `multica attachment upload
<local-path>`" — and diverge exactly where the platforms diverge: web chat
says the file "appears as an attachment card below it" and offers a markdown
snippet to place it inline; WeCom says Multica "sends it into the WeCom
conversation as a separate message right after your text" and that "there is
no way to place it inline". That last clause is the one worth having. An
agent told only that files work here writes "see the chart below" and then
nothing appears below, because the long connection has no msg_item and a
picture cannot be embedded in the reply. Slack and Lark keep their existing
text-only wording untouched.

Two comments are rewritten rather than carried over, because they asserted
the binary the same commit falsifies — "keys off 'is there a channel at
all'" in prompt.go, and "any non-empty channel type means the reply leaves
Multica for an external IM platform, where `attachment upload` has nothing
to bind to" in the brief. A comment that contradicts the code beneath it is
worse than none.

ChannelCarriesFiles names every channel type this package knows rather than
leaving Slack and Lark to the default, so a fourth adapter surfaces here as
a decision to make. An unrecognized type answers false: the agent is told to
describe its file in words, and the worst case is a deliverable that could
have been sent was not. False the other way would have it promise a delivery
nothing performs.

* fix(wecom): stop claiming more certainty than the system has

Three review blockers, one shape: a promise made in one place that another
place cannot keep.

Capability came from the channel type. The daemon answered "this conversation
takes files" from `chat_channel_type == "wecom"`, but the last hop only exists
where the server wired object storage — so a WeCom deployment without storage
had its agents told to send files nothing would carry, and a new daemon on an
older server inferred a capability that build has no code for. The server now
answers it per turn on the claim (`chat_channel_delivers_files`), declared by
the same `if store != nil` branch that gives the adapter the storage, and
absent/false everywhere else.

Upload limits came from the framing. 50 MB is what 100 chunks of 512 KB can
describe; WeCom's own OpenClaw plugin puts the hard file ceiling at 20 MiB, so
that is now the cap, enforced against the attachment's recorded size before any
byte is read. Concurrency was a fixed 4 regardless of size; WeCom's own SDK
steps it down — all at once up to 4 chunks, 3 up to 10, 2 past that, because
their backend answers a burst of chunks on a large upload with a system error.

Failure handling had two words for three outcomes. A send whose ack never came
was reported as a definite failure although the file may well be in the chat,
and the local failures — a full pending queue, a slot wait that expired, a
lookup that errored — were reported to nobody at all. There are now three
states: delivered says nothing, definitely_failed keeps the definite wording,
and unknown gets wording that holds either way and is never resent, since a
duplicate file cannot be taken back. The attachment lookup moved ahead of the
rationing so a shed delivery knows there was a file to apologise for, and a turn
with nothing attached still says nothing.

* fix(wecom): bound the attachment lookup, and stop calling an attempted write a failure

Two gates, not one better place for one gate. The pending counter is claimed
after the lookup so a shed can name a real file, which is also why it cannot
bound the lookup: at the point it would have to be claimed, whether the turn
carries a file is exactly what is not yet known.

admitAttachmentDelivery is claimed before the spawn and bounds the goroutines
and the queries they run. Its cap is twice the pending cap, so a backlog meets
the honest post-lookup shed first and only reaches the silent one when the
system is already well past it. A refusal there is logged and not spoken —
telling a user their file was dropped on a turn that may have carried none is
the false alarm the post-lookup gate exists to prevent.

Separately, a WebSocket write error stopped being read as proof of
non-delivery. wsSender.write already distinguished the stages for tracing; it
now carries that out in the error. Past WriteMessage the frame may be at the
peer — a half-closed connection reports "broken pipe" to the writer for bytes
the reader has — so those classify as unknown and are never resent. What fails
before the write is provably local and stays definite.

Tests drive the real path rather than the counters: one blocks the lookup,
submits past the cap, and counts the goroutines and queries that actually
happen; one hands the frame to a connection that records it and then fails.
Both fail without the change. fakeObjectStore takes a lock, because it is no
longer only ever driven inline.

* docs(wecom): cite the size limits to WeCom's docs, not a third-party constant

The upload ceilings were sourced to Tencent's OpenClaw plugin. That file
carries the right numbers but cites nothing itself, so it corroborates rather
than establishes them. WeCom documents all four on aibot_upload_media_init's
body.total_size row, and repeats them on the classic media upload API. Point
at those.

* fix(wecom): drop the two media helpers #6605 brought in ahead of this branch

This branch carried its own cleanShort filename and extension helpers because
nothing else in the package had them when it was written. #6605 has since
merged, and it brings both — so the package now declares each twice and does
not compile against current main. GitHub still reported the branch mergeable
and CI was green, because both were measured against a main that predated that
merge by six hours.

Upstream's copies are the ones to keep: its cleanMediaFilename also strips
control characters out of a name, and its mediaExtension drops a charset
parameter before matching. Neither of those was in the copies here.

* fix(daemon): keep the file-delivery verdict out of the cached brief

chat_channel_delivers_files arrives on every claim and answers a deployment
question: does this server carry a file the last hop into the room. Upgrading a
server so it starts sending the field, or turning object storage on or off,
flips it under a chat session that is already running — and the brief branched
on it, so one resumed session rendered two different briefs. The brief is the
prompt-cache prefix (MUL-5377), which makes that a miss on every turn after the
flip. The two rows in the delivery test asserting different briefs for the two
verdicts were the exposure written down: they are what one session sees in
sequence.

The brief's channel-backed chat copy now names the platform and says the answer
is stated per turn. It holds on either verdict and contradicts neither. The
verdict has a home already — the per-turn chat prompt, whose branch was correct
and is untouched — and that message is emitted on every turn, so nothing is
lost by the brief declining to answer.

Slack and Lark take the same copy. Scoping it to WeCom would put a channel-type
decision back into the brief, which is what ChannelCarriesFiles exists to
prevent, and would hardcode a denial a server-side change can falsify.

Coverage moves rather than goes. TestBriefByteIdenticalAcrossRunsForEveryKind
gains a WeCom kind and a variant that flips the field; both fail without this
change, and the existing Slack kind fails on the variant too. The delivery
test's two WeCom rows assert the inverse property now — one brief for both
verdicts — and the upload/describe split they used to hold is pinned by
TestBuildChatPromptTwoLayerChannelPolicy on the per-turn prompt, which has
covered both branches all along.

* docs(wecom): correct what the admitted cap implies, and note that delivery cannot answer back

The comment on the two counters said reaching the admitted cap means the
pending cap is full and as many turns again are still in their lookup. It does
not. Admission is held for a goroutine's whole life, including turns whose
lookup finds no file and which therefore never claim a pending slot —
TestDeliverAttachments_AdmissionBoundsTheLookupStage fills admission with
pending at zero. The two-counter split and the ordering it buys are unchanged:
a backlog of file-carrying turns still meets the honest post-lookup shed first.

The package doc gains the fact that an agent cannot learn its delivery was
refused. `multica attachment upload` returns once the object is in storage and
bound to the reply, while the send into the room runs on EventChatDone, after
the run has ended, so everything the adapter knows about a failed delivery goes
to the person in the chat and to nobody else.
2026-08-11 19:01:34 +08:00
545d890dae feat(cli): add --compact to issue comment list, adopt it in every agent read command (MUL-5442) (#6546)
* feat(cli): add --compact to issue comment list, adopt it in every agent read command (MUL-5442)

The #5999 follow-up its closing note asked for, now that the workspace
measurements exist: the two bounded reads the agent workflow prescribes
are dominated by reader-noise metadata, and every byte of it enters the
conversation history at full price once and is replayed at cache-read
price by every later call of the session.

--compact (JSON output only, opt-in) drops per comment: the issue_id
echoed from the request path, source_task_id, updated_at when identical
to created_at, null-valued fields, and empty arrays. Content, identity,
and thread-summary fields pass through untouched; a real edit timestamp
or non-empty array is never dropped.

Measured on MUL-5442 (production data): --roots-only --summary scan
6,117B -> 4,738B (-23%); --thread --tail 30 deep read 47,359B ->
39,349B (-17%).

Adoption: all nine runnable comment-list command templates in the
per-turn channel (prompt.go, reply_instructions.go) now carry --compact,
and the brief's Available Commands line and workflow step 3 document it
(+~40B one-time, byte-stable). Command pins updated accordingly; new
unit test pins the drop/keep contract and the help-contract test pins
the flag's rendered usage.

Known local-env failure: TestResolveWorkspaceID_AgentContextSkipsConfig
fails identically on clean origin/main in this agent runtime (agent
context env interference) — unrelated.

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

* test(cli): wire-level --compact coverage and zero-value keep pins (MUL-5442)

Review nits on #6546: TestRunIssueCommentListCompactWiring drives
runIssueCommentList against an httptest server both ways — default JSON
keeps every field, --compact drops the reader-noise set while zero-value
scalars (reply_count: 0, content_truncated: false, folded_count: 0)
survive, pinning that null pruning never generalizes to zero pruning.
Same zero-value pins added to the pure-function test.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-07 12:41:21 +08:00
73ab498d1d MUL-5811: fix(squad) derive leader role from wire fields, not instructions text (#6519)
* fix(squad): derive leader role from wire fields, not instructions text (MUL-5811)

The daemon decided whether a task ran in the squad-leader role by grepping
the agent's Instructions for the "## Squad Operating Protocol" heading that
the server-injected briefing starts with. Role detection therefore depended
on user-writable Markdown: any ordinary agent whose own instructions happened
to contain that heading was promoted to leader and handed the leader rules —
mandatory `multica squad activity` calls, a licence to exit silently on
no_action, and the squad-maintenance CLI section in its brief.

The correct signals were already on the wire the whole time. The claim
handler decides injection from explicit fields (`task.is_leader_task` +
`squad_id` for issue-bound runs, the quick-create `squad_id` for the picker
path) and forwards both to the daemon. Read those instead:

- `taskIsSquadLeader` returns `IsLeaderTask || SquadID != ""`; brief assembly
  now shares that helper instead of repeating the substring check.
- The claim handler clears `is_leader_task` on the response whenever its
  defensive gate withholds the briefing (NULL squad_id, squad hard-deleted,
  leader swapped after enqueue). The flag now means "briefing injected", so a
  leader task with no roster and no protocol degrades to an ordinary agent
  turn instead of booting into a leader role it cannot perform.

Every legal path is byte-identical: the server sets these fields on exactly
the claims it injects into, so real leader runs and ordinary runs produce the
same brief and prompt as before. The only behavior change is the misdetection
case, which is the bug.

Also corrects two comments claiming IsSquadLeader is agent configuration
rather than a per-task role, and notes the flag contract in the squad
source map.

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

* fix(squad): gate leader-role field reads behind a server capability (MUL-5811)

Review catch (#6519): reading is_leader_task / squad_id unconditionally
breaks the new-daemon → old-server direction the repo deliberately supports
(claimTasksLegacy, MUL-4257). is_leader_task only reached the claim response
in #4951, while briefing injection predates it — so against an older server a
real issue-bound leader task arrives with the full briefing and both fields at
zero, and field-only detection would silently demote that leader to a plain
worker, dropping the entire operating protocol.

Add `leader_role_resolved`, a claim-only wire capability the server sets on
every claim response (leader or not). Its absence is what identifies a server
too old to have answered the role question:

- capability present → the explicit fields decide, and an ordinary agent whose
  instructions contain the briefing heading is no longer promoted to leader;
- capability absent → keep the legacy briefing-marker inference, which is
  exactly today's behavior against those servers. That branch goes away once a
  minimum server version is enforced.

The field is claim-only and never rendered into a prompt, so it costs no
prompt tokens and changes no brief or prompt bytes on any path.

Tests: the role table now covers both server generations, including legacy
real-leader (heading, no capability), current ordinary-agent-with-heading, and
current withheld-briefing; an end-to-end legacy prompt test pins that an old
server's leader keeps its rules; and every claim test now asserts the server
actually advertises the capability — losing it would send all daemons back to
text inference.

Also rewords two comments per review: the brief byte-stability tradeoff is
owner-accepted and recorded in MUL-5811, not an open tracking item.

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

* docs(squad): state the pre-capability server shapes precisely (MUL-5811)

Review nit: the comments said every server without leader_role_resolved
"never sent is_leader_task", which is only true before #4951. Servers between
#4951 and this change do send the flag — they just never promised it implies
an injected briefing, which is the actual reason the fallback covers them too.

Names both shapes at each site (daemon types, handler response, helper doc,
role-table doc, squad source map) so a future cleanup of the fallback reads
the version window correctly. Comments only; no behavior change.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-06 21:05:21 +08:00
d6d1996210 refactor(daemon): retire the generic no-reply rule — ordinary agents always post one comment (MUL-5442) (#6493)
* refactor(daemon): retire the generic no-reply rule — ordinary agents always post one comment (MUL-5442)

Owner decision (MUL-5442, 2026-08-06): the no-reply / silent-exit
behavior is squad-leader-only by design. Mechanically it always was —
computeCommentAgentTriggers routes agent-authored comments only via
explicit @mentions (the sole implicit wake is the squad-leader path), so
loop prevention lives in the Mentions discipline, and the generic rule
only suppressed one terminal noise comment on an already-enqueued run
while giving every ordinary agent a legal silent-exit path that risks
silently dropped results.

- prompt.go: delete the per-turn agent-trigger warning block entirely
  (−728B per agent-triggered turn; supersedes #6484's compression).
- runtime_config_sections.go: delete the Reply-mode reply-warranted
  bullet and step 5's conditional pointer; the reply bullets become
  unconditional (−472B one-time in the brief). Squad-leader no_action
  bullets and the Mentions section are untouched.
- reply_instructions.go: 'If you decide to reply, post it' → 'Post your
  reply' (−16B per Reply turn); the leader's no_action block states its
  own exception.
- Tests: retired pins are converted to negative guards with ledger
  comments (brief and per-turn both must NOT carry the retired
  apparatus); attribution, member-leak, and no-MUST-respond guards stay.

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

* fix(daemon): scope the reply imperative with the leader's no_action carve-out, close the pin-ledger gaps (MUL-5442)

Review catches by Elon on #6493, both accepted:

1. The leader's only silent path was contradicted later in the same
   prompt: the no_action rule said EXIT without commenting, then the
   cookbook and the brief's mandatory bullet said post unconditionally.
   Both now branch on squad leadership — agent configuration, never
   per-run state, so each agent's brief stays byte-identical across
   turns and issues (cache-safe): leaders get 'Unless your outcome is
   no_action, post your reply…' in the cookbook and 'Unless your outcome
   is no_action (Squad leader rule above), posting your reply as a
   comment is mandatory' in the brief; ordinary agents keep the
   unconditional wording byte-for-byte. New tests render the COMPLETE
   leader prompt and leader brief and assert the carve-out is present
   AND the unconditional capital-P imperative is absent — the scope
   relation, not two isolated Contains.

2. The retirement ledger named five brief pins but guarded only two,
   and the per-turn 'If you decide to reply' had no guard. Added the
   four missing negative guards in ordinary-agent scope (the leader's
   'DO NOT post any comment' matches none of them) plus a leader-leak
   guard on the ordinary brief.

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

* fix(daemon): scope the multi-thread fan-out under the leader's no_action carve-out (MUL-5442)

Ports the per-turn half of #6493 review rounds 2-3 into the slimmed PR
(the brief role-independence refactor moves to its own sub-issue/PR per
owner decision): BuildMultiThreadCommentReplyInstructions takes the
leader flag, and the leader lead sentence scopes the ENTIRE fan-out
block — every later obligation sits under the 'Otherwise' — with
index-order test guards. Ordinary fan-out output is byte-unchanged.

Also repairs a truncated TestFreshSessionMayHelp from the earlier
rebase conflict resolution (missing closing braces).

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

* docs: correct the taskIsSquadLeader comment — leadership is a per-task role (MUL-5442)

Review nit on #6493: the helper's comment still claimed leadership is
'agent configuration, never per-run state' — the disproven premise that
drove the earlier review rounds. State the per-task-role fact and point
at MUL-5811 for the role-independence work.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-06 16:12:22 +08:00
62a2d70afd refactor(daemon): stage-2 — Mentions and Comment Formatting judgment rewrite (MUL-5442) (#6453)
* refactor(daemon): rewrite Mentions and Comment Formatting to their judgment form (MUL-5442)

Stage 2, final two sections, bundled per the small-sections rule.

Mentions: the four-link side-effect table stays verbatim (platform
facts); the two H3 subsections merge into one policy paragraph keeping
every anti-loop anchor — the no-mention default with its cost mechanism,
the no-sign-off-mention ban, the end-with-no-mention rule, the three
mention-warranted cases, and the silence closer. The retired headings'
pins re-anchor to the policy phrases.

Comment Formatting: both variants keep the full operational contract
(file-first sentence verbatim, both bans with incident ids, workdir
scope, --parent continuity, cleanup, newline rule); what goes is the
mechanism narration (what the shell rewrites, how flags get swallowed,
PowerShell version/encoding detail — the consequence stays).

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

* fix(daemon): correct the PowerShell version claim, pin the mention scope qualifiers, section-scope the formatting assertions (MUL-5442)

Review catches by Elon on #6453, all three accepted:

1. The compressed Windows rationale over-generalized a version-specific
   fact: '$OutputEncoding drops non-ASCII' is true of Windows PowerShell
   5.1 (ASCII default), false of PowerShell 6+ (utf8NoBOM). Now reads
   'Windows PowerShell 5.1 ... may replace non-ASCII characters with ?';
   the Go comment documents the version split and why file-first stays
   version-agnostic (agents cannot rely on which shell services the pipe).
2. The merged Mentions paragraph was pinned only at the list head — the
   scope qualifiers ARE the anti-repeat-notify boundary: 'not yet
   involved', 'for the first time', 'explicitly asks to loop someone in',
   and the loop-cost mechanism are each pinned individually now.
3. The Comment Formatting assertions ran against the whole file, where
   '#4182' also appears in Available Commands — the HEREDOC ban could
   vanish with green tests. The assertions now slice the section (matched
   at the line-start heading, since Available Commands references the
   heading inline) and cover all seven contract elements within it.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-06 12:44:09 +08:00
f44df04f96 refactor(daemon): rewrite the Output section to its judgment form (MUL-5442) (#6425)
Stage 2 section 2. The delivery contract keeps every platform fact and
boundary: comment-add as the only delivery channel, the invisible
terminal, exactly one comment per run before turn exit, --attachment as
the only file path, the runtime-local-path ban with the code-location
form and the say-so-in-words fallback. What goes is derivation: the
invisible-task consequence restatement, the plans-in-your-reasoning
elaboration, the good/bad style examples, the exists-right-now clause,
and two quick-create explanation tails.

One pin re-anchor: 'Do not assume any workspace issue prefix' follows the
rewrite to 'never assume a workspace issue prefix'. Every other Output
and delivery-invariant pin passes unchanged, including the MUL-4899 trio
and the anti-dangling pointer target the Attachments section names.

Issue-kind Output: 1,493 -> 978. Brief (real-UUID fixture):
13,382 -> 12,907 (-475); quick-create variant sheds ~160 more on its own
kind.

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-05 21:15:13 +08:00