mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-10 02:16:45 +08:00
admin-workspace-create2
191
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]> |
||
|
|
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]>
|
||
|
|
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]> |
||
|
|
0a979b1f9c |
fix(search): classify client cancellation separately from server errors (MUL-7989) (#9209)
* fix(search): classify client cancellation separately from server errors * ci: rerun checks for search cancellation fix |
||
|
|
e7742b052b |
MUL-7776: fix(api): stop returning raw error text in 500 responses (#8905)
* fix(api): stop returning raw error text in 500 responses 39 handlers answered a 500 with the message followed by err.Error(), sending database and store errors (constraint names, SQL states, Redis errors) to API clients, while the other 720 500 responses in the handler package send a fixed message. These now send their fixed message too and log the error with the request's attributes, as the handlers that already logged it do. * fix(api): drop raw error text from the task callbacks and skill import 500s completeTask and failTask sent err.Error() as the whole 500 message, and the structured skill import put it in the result's Reason on its three 500s. They now send fixed messages and log the error. The task callbacks keep their 500, so the daemon still retries them: isTransientError reads the status code, never the body. * fix(api): close remaining raw error leaks Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Yushen <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
647b18b18a |
fix(search): recognize digit-containing issue prefixes across search paths (#9101)
Co-authored-by: multica-agent <[email protected]> |
||
|
|
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]>
|
||
|
|
4469bac632 |
fix(issues): duplicate mark follow-ups from #8741 review (MUL-7349) (#8752)
* perf(issues): resolve a page's duplicate originals in one read (MUL-7349) Filling duplicate_of read each distinct original with its own primary-key lookup, so a page of duplicates of different issues cost one round trip per row. Pages now pass the originals their rows point at to newStatusCategoryFiller, which loads them with one ListIssueRefsInWorkspace read, the same way labelsByIssue loads a page's labels. List, open issues, grouped, search and table rows do this; a response built on its own still resolves its original, so a caller that passes nothing is slower, never wrong. A pointer left on a reopened issue is not looked up at all, and an original that no longer exists is looked up once and left off. Co-authored-by: multica-agent <[email protected]> * fix(issues): stop logging stale duplicate pointers as unmarks (MUL-7349) Servers from before the duplicate column reopened duplicates and deleted originals without clearing the pointer. After an upgrade, the next write to such an issue clears it, and the activity log compared raw pointers, so it logged "removed duplicate mark" for a mark that no longer counted and dropped the real status row in its place. Event publishers now send only live marks: a pointer counts while the issue is cancelled, the same rule reads apply. Separately, the status row is dropped only when a duplicate row was actually written to stand in for it; when the original is gone there is no duplicate row, and the reopen used to leave no activity at all. Co-authored-by: multica-agent <[email protected]> * fix(issues): keep duplicate marks in background issue snapshots (MUL-7349) IssueToMap hardcoded duplicate_of to null on the assumption that no background publisher renders a marked issue. PublishAttachmentsChanged does: a channel /issue with media re-reads the issue after a download of up to 45 seconds, and a mark made in that window was broadcast as null. Clients patch their cache with that snapshot, so the list and board marker disappeared until the next refetch. IssueToMapResolved now resolves duplicate_of the way the HTTP rendering does; only a cancelled issue with a pointer costs a read. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
1539c54ec5 |
feat(issues): make duplicate marks traceable and one click from the original (MUL-7349) (#8741)
* feat(issues): make duplicate marks traceable and one click from the original (MUL-7349)
- Log duplicate_marked / duplicate_unmarked on the duplicate and
duplicate_added / duplicate_removed on its original, with the other issue
linked in the activity feed; the delete and GitHub merge paths now carry
both ends of the mark so those rows are written too.
- Expose duplicate_of_issue_id on issue responses (only while cancelled) so
list rows, board cards and table cells can show an arrow to the original
that opens it directly.
- Banner links as a whole to the original and shows its status; the undo
action becomes the secondary "Not a duplicate" with a toast.
- Sidebar duplicates show reporters and a count; the status picker reads
"Change original" on an issue that already is a duplicate; the mark
picker opens with same-title and recently viewed candidates.
* fix(issues): keep the duplicate pointer in step with CI shape tests (MUL-7349)
- Add duplicate_of_issue_id to the CLI --fields whitelist, which the
list-endpoint shape test checks against a real response.
- Project the column in the search parity test's legacy query copy and
scan it, so both queries read through the one scanner.
- Mount the row marker's query and navigation hooks only when the issue
is a duplicate, so surfaces rendered without a QueryClient (the table's
inline title) and thousands of ordinary rows subscribe to nothing.
* fix(issues): resolve a duplicate's original on the server and log one row per mark change (MUL-7349)
- Issue responses carry duplicate_of {id, identifier, title, status}, filled
beside the status category only while the issue is cancelled and the
original still exists; the bare pointer is no longer exposed. Rows read it
directly, so the marker makes no request and is a real link where the row
is not one.
- A mark change suppresses the generic status_changed row; the unmarked row
records where the status went. Covered through the real UpdateIssue path,
with the test server wiring activity listeners like main.go.
- The picker applies one selectability predicate to search results and
suggestions, so an existing duplicate can no longer be chosen.
|
||
|
|
96db419c0a |
feat: mark an issue as a duplicate from the status picker (MUL-7349) (#8728)
Enable step. The previous release added duplicate_of_issue_id and the
code that keeps it correct; this one lets people create a mark.
PUT /api/issues/{id} accepts a write-only duplicate_of_issue_id, which
sets status to cancelled and so reuses every status side effect
(status_changed event, stage barrier, wakeups). Marks are validated
under row locks on both issues: the target may not itself be a duplicate
and an issue with duplicates may not be marked, so the relation stays
one level deep and concurrent cross-marks cannot form a cycle.
GET /api/issues/{id}/duplicates returns both sides, and applies the
validity rule itself so a rollback past the previous release still reads
correctly.
The status picker on an existing issue gains a 'Mark as duplicate...'
action in its footer, outside the arrow-key listbox so it is never
mistaken for a status. A duplicate shows a banner linking to its
original with an unmark action; the original lists its duplicates.
Copy added for en, zh-Hans, ja, ko and fr.
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
b7cdd76ad8 |
MUL-7349: feat(server): add the duplicate-mark column and keep it correct (1/2) (#8691)
* feat(server): add the duplicate-mark column and keep it correct (MUL-7349) Expand step for marking an issue as a duplicate of another. A duplicate is an ordinary cancelled issue that records which issue it duplicates; there is no duplicate status. This release adds the column and the code that maintains it, and nothing writes a pointer yet. - A mark survives only a write that leaves an already cancelled issue cancelled. Every other write drops it, so reopening removes a mark and cancelling again does not bring one back. Both status-writing queries carry the rule, which covers the UI, batch updates and background writers. - Deleting an issue clears the pointers of its duplicates in the same transaction, the way deleting a parent detaches its children. Shipping this before marking is what makes marking safe to roll back: the release a rollback lands on already clears the pointer. Tests seed a mark the way the next release will write it. Co-authored-by: multica-agent <[email protected]> * fix(server): compute issue updates from the locked row (MUL-7349) UpdateIssue built its next_* values in a CTE that read the row before the UPDATE took its row lock. A write that waited there behind another transaction then resumed with the values it had read before the wait and wrote them back, so an ordinary priority or status edit could restore a duplicate mark the other transaction had just cleared — by reopening the issue or by deleting its original — and did_change/did_activity were decided against the pre-wait row as well. Lock the row in the CTE instead, so every computed value comes from the row the statement is about to write. Tests hold the clearing transaction open and wait for the update to block on the row lock, so the interleaving is deterministic; both fail without this change. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
90e0bdf830 |
MUL-7586 Atomically create issues with custom properties (#8689)
* feat(issues): create with properties atomically Co-authored-by: multica-agent <[email protected]> * fix(issues): recover unavailable properties in create drafts Co-authored-by: multica-agent <[email protected]> * test(issues): isolate commit failure task assertions Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Forge-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
bd70632999 |
fix(wakeup): end closed-issue wakeups in the application, not a trigger (MUL-7557) (#8642)
Migration 518/520 (#8467) registered stop_issue_wakeups, an AFTER UPDATE OF status trigger on issue that disabled issue_wakeup rows and cancelled queued/deferred wakeup runs in agent_task_queue. That is a database-level cascading update, which the repository's migration rules forbid. Drop the trigger and function (migration 533) and apply the same close rule in the transaction that writes the status: single and batch issue updates and the merged-PR auto-close. Cancelled runs are now broadcast after commit. Co-authored-by: multica-agent <[email protected]> |
||
|
|
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]> |
||
|
|
d7009df8cc |
Revert "MUL-7300 fix(issues): preserve Quick Create original input (#8519)" (#8546)
This reverts commit
|
||
|
|
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]> |
||
|
|
c7f259c70a |
revert(server): withdraw the reserved triage status key (#8450)
The `triage` status key was reserved so the server could read `status = 'triage'` as "in Triage": migration 475 added a CHECK against it, 476 renamed any pre-existing custom `triage` status (and the issues and saved views pointing at it), and issuestatus refused the key on every write path. Migration 483 then moved Triage onto issue.triage_state, and nothing reads the status that way any more. What is left is a rewrite of live customer data in the deploy path that buys nothing, plus a name the product does not need to own — users see a status's display name, not its key, so reserving the key never stopped anyone from calling a custom status "Triage". 475-477 are emptied rather than reverted with a new migration: they have never run outside development and CI, because releases deploy from tags and the newest tag predates them. Migration 486 drops the constraint on databases that did migrate off main, which would otherwise refuse a key the server now accepts; 476's rename is not reversed, since the replacement is an ordinary custom key by then. 478 loses the matching `triage` fast-path entry, where the key falls through to the same answer either way. The Go side goes with it: the Triage constant, ErrReservedStatus, the Resolve refusal, IsBuiltIn's extra name, ErrStatusReservedForTriage and its two handler mappings, the leading Triage rank in the issue sort, and the CLI and docs lines that told users the key was reserved. Every triage_state path is untouched. Tests now pin the reverted behavior: the emptied migrations leave a workspace's custom `triage` status, issues and saved views exactly as they were, 486 drops the barrier on a database that already has it, and `triage` works end to end as an ordinary custom status key. Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
7ebe0bf58d |
MUL-7213: feat(server): Triage has no executor (#8373)
* feat(server): start no run for an issue in triage (MUL-7213) An issue waiting in Triage produces no execution run, and the triage run itself leaves no trace an execution run can inherit. The rule cannot be enforced at the call sites. Five paths enqueue without looking at status at all (comment and mention dispatch, quick action, manual rerun, auto-retry), and dispatchIssueRun discards the enqueue error entirely, so a missed call site would fail silently. So the check sits at the queue door — immediately before every INSERT of an issue-linked agent_task_queue row — and the upstream short-circuits exist only so that previews and dispatch reasons stay honest about what the door will do. guardIssueNotInTriage covers all five insert sites: both enqueue funnels, FailTask's in-transaction retry, the orphan sweeper's retry, and the delegated-failure recovery dispatch. Two future paths are meant to be exempt and both are Triage's own — the triage run, and accept, which leaves Triage in the same transaction that enqueues the execution run. Upstream, WillEnqueueRun, maybeEnqueueOnAssign, the two shouldEnqueue*OnAssign gates and the comment trigger router all refuse Triage; rerun and quick action refuse with 403 issue_in_triage, a new dispatch reason code. WillEnqueueRun also treats leaving Triage as a creation rather than a status change: accept is when the issue becomes work, and the backlog-to-elsewhere rule would refuse it outright. Two narrower rules follow. retryEligible excludes triage runs for the reason it excludes autopilot runs: re-triaging reads the entry as it stands now, so replaying a failed attempt is never the right recovery. And GetLastTaskSession excludes them from all three CTEs, so the first execution run after accept does not resume the conversation in which the agent was still deciding whether the entry was worth keeping. Refusals are deliberately quiet where a caller cannot act on them. The FailTask guard never returns — that transaction also carries the parent's failed status, and losing it would leave the task stuck in 'running'. Verified: internal/handler, internal/service and cmd/server pass; every new test was confirmed to fail with the change neutralized. Co-authored-by: multica-agent <[email protected]> * fix(server): close two triage session bypasses around rerun (MUL-7213) Review found that session isolation had two holes the query-level exclusion could not reach, and that the two quietest queue-door paths had no direct coverage. A manual rerun resolves its session from the task the user named, via rerun_of_task_id, not from GetLastTaskSession — so after accept, naming the historical triage task reran the triager and resumed the triage conversation, walking straight around the exclusion. RerunIssue now refuses a triage source: redoing triage is re-triage, a different action on a different status. rerunSourceMatchesTaskScope refuses it too, which is what still holds for a rerun row an older server wrote mid-deploy. GetLatestTaskRolloutMissing reports only whether GetLastTaskSession fell back, so the two have to exclude the same rows. It did not, so a terminal triage run with session_rollout_missing told the first execution run after accept that it had lost the previous turn's context — about a turn it was never entitled to. Tests now cover the two doors that refuse silently: FailTask's in-transaction retry, which must leave the parent failed and add no child, and dispatchDelegatedFailureRecovery, which builds its coordinator task with a direct CreateAgentTask call. Both assert against a positive control so a zero cannot come from an inert fixture; the recovery test also shows the obligation is deferred rather than dropped — the same pending comment wakes the coordinator once the issue is accepted. Verified: internal/handler, internal/service and cmd/server pass; all five new tests were confirmed to fail with the fixes neutralized. Co-authored-by: multica-agent <[email protected]> * refactor(server): let a named agent talk on a Triage issue (MUL-7213) The rule shipped as "an issue in Triage starts no run", which is a mechanical restatement of the product red line rather than the line itself, and it was too wide: @-mentioning an agent to discuss a Triage entry was refused along with everything else. What Triage actually lacks is an EXECUTOR. Nothing may derive one from the issue — the assignee, a prefilled owner, a squad leader, an automatic retry — because that is exactly what the triager is still deciding, and because prefilling an owner is how a run starts today, the triager would otherwise dispatch the very owner it was only proposing. A person naming an agent by hand is a different act: no triager action produces it and the cost is one the person chose. That is the same shape as backlog, which parks assignment but lets comments through, one notch tighter because in Triage the assignee is only a proposal. So the queue door now judges the run's origin rather than the funnel it came through. OriginDerived is the zero value, so a caller that says nothing still gets the strict answer and a new enqueue path cannot leak in by omission; the two mixed wrappers take it as a parameter, which made the compiler enumerate every call site. Comment routing loses its blanket short-circuit and instead refuses the two routes that derive an executor — the assignee fallback and the squad leader fallback. Explicit mentions, replies to an agent, and thread continuation route as they do anywhere else. Rerun now follows its source: a discussion run may be repeated, naming no source means "run the assignee again" and is refused, and the triage run itself keeps its own reason. Assignment, quick action, auto-retry and all three session isolations are unchanged. The delegated-failure door is gone because main's #8406 gate subsumes it: Triage is its own lifecycle category, so it is neither unstarted nor started and both that gate and the pending-recovery query already exclude it. A test pins that. Verified: internal/handler, internal/service and cmd/server pass; both halves of the new rule were confirmed to fail when neutralized in place. Co-authored-by: multica-agent <[email protected]> * refactor(server): hold Triage in its own column, not in status (MUL-7213) `status` answers "how far along is this work". Triage answers "is this work at all". Holding the second question inside the first field makes every reader of `status` re-answer it, which is where the scattered `<> 'triage'` checks came from — main carried 9 of them and this branch had grown to 23. A column of its own asks it once. issue.triage_state is NULL for an ordinary issue and `pending` for one waiting in Triage. Adding a nullable column with no default is a catalog-only change, so no table rewrite and no backfill; the queue reads a partial index sized to itself rather than to the issue table. What follows from the move: - issuestatus goes back to 7 keys and 4 categories. Triage is no longer a category, a behavior, or something ExpandCategories can name, so CategoryForBehavior, BehaviorsForCategory, ParseCategory and ExpandCategories lose their special cases, and the dead board-order arm and status label go with them. - WillEnqueueRun loses `acceptedFromTriage`. Accept clears a column and then takes the ordinary create / assign path, so there is no "was in triage" transition left for the predicate to recognise. - The write lock disappears rather than narrowing. Status, project and parent on a Triage entry are the triager's proposal and accept is what confirms them, so ordinary writes reach them; the marker itself is not a field of the issue API, so there is nothing left to lock. - The duplicate guards carry `triage_state IS NULL` instead of a status key, and the delegated-failure recovery predicate gains the same, in both its SQL and its Go mirror. What does NOT follow, deliberately: the reserved NAME stays. Migration 475's CHECK is deployed and 476's renames are irreversible, so `triage` remains unavailable as a custom status key and Resolve keeps refusing it — one reserved word rather than five special cases. Read-path exclusion is a separate sub-issue and is not attempted here. Also from review: the quick-action refusal is now documented as a first-phase product choice rather than a consequence of the rule (its target is configured, not derived), and ErrIssueInTriage no longer claims that nothing runs. Verified: handler, service, issuestatus, cmd/migrate and cmd/server pass on a database migrated from 001; every column read was confirmed to turn its own tests red when neutralized. cmd/migrate's concurrent-index convention test caught a missing cleanup hook for the new index. Co-authored-by: multica-agent <[email protected]> * fix(server): hold the parent of a Triage entry (MUL-7213) Removing the write lock went one field too far. Status, project, assignee and the rest on a Triage entry are the triager's proposal and accept confirms them, so those writes belong. The parent is not a proposal: - the routing panel has no parent field. "Merge into an existing issue" links rather than re-parents, so a Triage entry hanging off a parent is a half-state the product never asks for; - a child in Triage carries a proposed status, so it is never terminal. stageBarrierClosed would wait on it forever — holding the whole sibling set, or one stage's frontier — and ChildIssueProgress would count it in the denominator. Both reach it through parent_issue_id, so refusing the write is what keeps them out of reach. That is behavior, not display, and it is why this one is worth a lock rather than a read-path filter. UpdateIssue and batch refuse a request that carries parent_issue_id for an issue in Triage, with or without a value: null clears, which is equally a parent decision the triager does not get to make. The batch asks one count rather than listing statuses, since "any" is all it needs. Verified: handler, service, issuestatus, issueguard, cmd/migrate and cmd/server pass; both new tests were confirmed to fail with the guard neutralized. Co-authored-by: multica-agent <[email protected]> * fix(server): drop the triage case from the handoff predicate (MUL-7213) MUL-7379 (#8435) landed on main while this branch was open and added issueStatusIsHandoff with a `triage` case, resting on Triage resolving to a category of its own without a catalog read. Once Triage stops being a status that resolution is gone, so the call errors on a value no issue can hold any more. The case is removed rather than repaired: a Triage entry carries whatever status the triager has proposed and is answered on that. Suppressing notifications for Triage entries is a different rule and belongs to MUL-7219 — the status model was giving it incidentally, and that was never the mechanism the design asks for. Verified on a database migrated from 001: cmd/server, internal/handler, internal/service, internal/issuestatus and cmd/migrate pass. Co-authored-by: multica-agent <[email protected]> * chore(db): renumber the triage migrations past the telemetry batch (MUL-7213) MUL-7236 (#8252) took 479-482 on main while this branch was open, so both triage migrations collided. 483 adds issue.triage_state, 484 builds its partial index; the concurrent-index cleanup hook moves with it. Verified on a database migrated from 001 with the new numbering. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
ff60b2f25a |
MUL-7379 fix: restore per-status sort order and the delegated handoff signal (#8435)
Fixes three regressions left by MUL-7240's collapse from seven stored issue
lifecycle categories to four. All three share one shape — a comparison of an
Effective() result against one of the five NON-TERMINAL built-in keys, which
stopped matching custom statuses once nonterminal custom keys were left raw.
Terminal projection survived, which is why no done/cancelled check regressed.
Principle applied: behavior logic aggregates on category, display logic resolves
on status.
1. Status sort collapsed from seven ranks to four (server and client, affects
every workspace). Backlog tied with Todo and In Progress/In Review/Blocked
all tied, leaving the created_at tiebreak to order a list the user sorted by
status. Both sides now rank on the concrete key in catalog board order,
reusing issueTableStatusOrder so sorting and grouping share one source of
truth. statusOrderExpression itself is unchanged — its other caller ranks
the four category buckets of a status_category grouping, which is correct.
2. Delegated subscribers stopped being told about handoffs. Their allowlist was
{in_review, done, cancelled, blocked}; a workspace's "Awaiting Response" or
"Code Review" used to match via its category and no longer did, so the human
whose agent filed the issue was never told it was waiting on them. Both
hand-kept allowlists (which had already drifted) are replaced by one
predicate: done/closed, or started excluding the fixed in_progress key.
Delivery fails open, stale task_failed dismissal fails closed.
3. Autopilot webhook crash-compensation stopped repairing the lost create_issue
task for any workspace with a custom status, leaving the run in
issue_created with no task while the delivery recorded success.
Also corrects four comments that still justified their behavior with the
retired "a custom status resolves to the canonical status it inherits"
mechanism — the exact failure mode MUL-7364's postmortem named.
Tests: the existing sort regression test used one issue per category, so four
buckets and seven produced identical output; the new one puts several built-ins
in one category, wedges a custom Started status between two of them by position,
and orders insertion so created_at DESC is the reverse of the expected result.
Verified to fail on the pre-fix expression.
|
||
|
|
8908fcfbc4 |
feat(server): reserve the triage status key (MUL-7212) (#8298)
Make `triage` a platform-reserved issue status: it has no catalog row, is not a category a custom status can declare, and no ordinary write can set it or move an issue out of it. - issuestatus: Triage constant; IsBuiltIn covers it, so ValidateKey, DeriveKey and firstFreeKey refuse or skip the key; Resolve refuses it with ErrReservedStatus; Effective and Category resolve it without a catalog read - migrations: a barrier file adds CHECK (key <> 'triage') NOT VALID on its own, so it commits before the repair file renames a pre-existing custom `triage` key per workspace (firstFreeKey rule) across the catalog, issues and saved views and VALIDATEs; issue_effective_status() gains 'triage' on its fast path - writes: status_reserved_for_triage on create/update/batch; issue_in_triage when status, project_id or parent_issue_id is written on an issue in Triage; merged PRs no longer auto-advance a Triage issue - status sort ranks triage first; duplicate guard ignores triage issues - notification label, CLI help, TriageV1 feature flag key - cmd/migrate forwards notices marked 'migration report: ' to its log Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
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]> |
||
|
|
3551e72e76 |
MUL-7232 feat(daemon): add dsh desktop(self-hosted) support (#8237)
* feat(daemon): discover the DSH CLI bundled in DeepSeek Harness Desktop DeepSeek Harness Desktop ships its CLI inside the .app and never installs `dsh` onto PATH, so a GUI-launched daemon missed it on exec.LookPath — and the login-shell sweep could not rescue it either, because no rc file has a reason to know that path. Only a hand-made symlink made dsh visible at all. Add the app-bundle fallback the Codex provider has had since #5205. The candidate is a Node script rather than a native binary, so it counts only while it is executable: an entry discovered without the executable bit would be advertised as a healthy runtime and then fail on every spawn. The Multica runtime profile gate still applies — a bundled CLI without the profile is not a runtime. * fix(daemon): report and install the missing DSH runtime profile probeAgentCLIs is documented as pure discovery, but dsh's runtime-profile gate lived inside it — one layer above the verdict machinery that turns a drop into a /health skipped_agents reason and a daemon-log line. A dsh binary without the profile therefore vanished from the runtime list with nothing anywhere saying why, which is the shape the DSH reports in #7721 and #6936 run into. Move the gate down to probeBuiltinRuntime and give it its own verdict: dsh is now discovered like any other CLI, and failing to use it is a reported, actionable drop. The verdict is deterministic, so it demotes immediately rather than waiting out a confirmation window — it is a fact about the CLI's environment, not about bytes mid-write. The executable is checked first, because probeDshMulticaProfile cannot tell "the profile refused" from "there was nothing to run", and would otherwise blame a missing profile for a path that vanished under us. Reporting is only half of it: DSH is the one built-in whose CLI is not enough on its own, and no other provider needs anything installed into the agent's own home. MULTICA_DSH_PROFILE_BUNDLE now names what to install — an npm spec, a directory or a tarball, candidates tried in order. Unset stays the default and means "do not touch the user's DSH installation"; there is nothing to point it at yet either, because Multica's own bridge is unpublished (#6936). DSH Desktop ships the pnpm that `dsh plugin` forwards to inside its own runtime-commands directory and injects it only for the processes it spawns, so the install prepends that directory to PATH. Without it the command fails with "pnpm not found on PATH" on exactly the machines this is for. The install runs once per daemon, and the discovery loop is kicked when it finishes so dsh comes online immediately instead of at the next tick. * fix(daemon): keep a registered dsh runtime in step with its runtime profile Two states have to agree: the Multica runtime profile is installed, and a dsh runtime is registered. Neither disagreement corrects itself, and both leave the server routing work into a CLI that cannot start. When the profile is removed, dsh stays discovered and keeps its runtime, so nothing else in the discovery loop looks at it again. Observed in the field: a profile removed a minute after startup stayed live for three minutes and a chat task failed against it. When a profile is installed by hand, the runtime is missing but the converge backoff has already doubled to agentConvergeMaxBackoff (30m) over the rounds that could not register it, so the fix stays invisible for up to half an hour. Compare the two states on every tick and force a round on disagreement, in either direction. A round forced for a registered provider has nothing missing to register, so convergeRuntimeRegistrations now consumes the verdict map it used to discard — safe because demoteUnusableRuntimes owns the claim barrier and the seq-stamped hold, and stays the single owner of demotion. The profile's presence is a stat of the manifest DSH itself looks for, so the check costs one syscall per tick; the authoritative --probe stays inside the probe round the check forces. * docs(runtime): document the DSH runtime-profile variables The two variables this branch adds were undocumented, and the install guide still told readers to run `dsh plugin --profile multica add <bundle>` without ever saying what <bundle> is — the hole #6936 was filed against. Document MULTICA_DSH_PROFILE_BUNDLE and MULTICA_DSH_PLUGIN_PATH next to the other MULTICA_DSH_* variables, and name the bundle in the DSH setup step: what implements the protocol, where the bridge lives, and that the daemon reports the missing profile on /health rather than registering a runtime that cannot start. All four locale files, so the translations do not drift. * refactor(agent): export the DSH stdio protocol version The daemon's runtime-profile probe accepts or rejects a profile on this exact number: one answering `--probe` with a protocol this backend does not drive must not be registered. That decision was about to be made against a second copy of the constant in internal/daemon, and a second copy is one that can drift from the code actually speaking the protocol. Export it so the probe reads the number from its only honest source. No behaviour change: the value is still 1. * fix(redact): mask credentials carried in URL userinfo The connection-string rule covers postgres, mysql, mongodb, redis and amqp only, so `https://user:token@registry/...` passed through untouched. That shape is not hypothetical for agent output: npm and pnpm accept an authenticated registry URL as a package spec, and both the package manager and the registry echo the request URL back in their error text, which is how it reaches a log line without anyone logging the spec itself. Add a scheme-agnostic userinfo rule. The scheme and host survive — which registry refused the request is the part an operator reads, and it is not the secret — and userinfo requires both a colon and an `@` before the next path separator, so `https://host:8443/p`, `ssh://git@host/p` and a bare `a:b` in a path are all left alone. Tests cover both directions. * fix(daemon): decide the DSH runtime-profile verdict on evidence probeDshMulticaProfile returned a bare bool, so a timeout, a non-zero exit, unparseable output, a future protocol version and a genuinely absent manifest all collapsed into "the profile is not installed" — a verdict that demotes a live runtime and, with a bundle configured, installs over the user's DSH home. This rebuilds the verdict layer around what was actually observed. Classification. The probe now returns one of OK / missing / incompatible / unavailable. The frame decides, not the exit status: a profile that printed a valid answer has answered, whatever the process did on its way out — which also stops a process-tree lifecycle error from being read as a protocol failure. With no usable frame, a cancelled ROUND stays transient (the daemon is shutting down; that says nothing about the profile) and otherwise the manifest decides. A manifest that IS present keeps every failure transient, which is what protects a working install during an upgrade. Timeouts are not transient by themselves. Asked for a profile it does not have, DSH does not refuse `--probe` — it hangs, printing nothing, until something kills it. Measured: still running after 10s, zero bytes on both streams. So a timeout is the ordinary way a missing profile presents, and treating it as transient made the profile undetectable on exactly the host the automatic install exists for. The manifest is the same file DSH's own loadProfile consults before refusing to boot, so a confirmed-absent one is evidence. Order. The profile probe now runs only after version detection succeeds. exec.LookPath is far too weak a proxy for "this CLI works": on Windows the CLI is a .cmd shim it resolves happily and which exits 9009 — cmd.exe's "command not found" — when what it forwards to is missing. Probing the profile first reported such a machine as missing its runtime profile, a repair for a problem it did not have. Demotion. An incompatible profile is reported and never demoted: this daemon can be the stale side of a protocol skew, and tearing down a live runtime because THIS build is behind is a destructive answer to a version question. The confirmation window that gated the not-executable verdict now gates every condemnable verdict, keyed per verdict so a provider moving between states starts a fresh clock rather than inheriting one earned by another problem. Convergence. A registered dsh whose profile was removed forces a round on every tick, because condemning it takes two of them and nothing else schedules the second: with dsh still registered nothing is missing a runtime, so an unforced tick returns without probing at all. The mirror direction — a profile with no runtime — stays throttled to one forced round, since no round is guaranteed to resolve it and forcing would bypass agentConvergeMaxBackoff forever. Offline cause. A missing profile reaches the server as a structured `dsh_profile` code rather than a bare offline, and carries whether the daemon is installing the profile right now — the one case where queueing is right. That claim is withdrawn if the install gives up: the runtime row is written once, by the demotion that also removes the runtime from the index, so nothing later would correct it and the work would queue forever behind an install that already stopped. Provisioning. Runs under the daemon's lifetime through processtree, so the pnpm/node descendants cannot outlive a shutdown and keep writing into the DSH home. Success is the manifest on disk, not the package manager's exit status, and a candidate that exits 0 without producing a profile is a failed candidate so the next one still gets its turn. The bundle spec no longer reaches a log line — an npm URL can carry credentials and a path a username — and captured output is redacted and bounded on a rune boundary. The PATH merge matches the variable case-insensitively, because Windows spells it `Path` and an exact `PATH=` miss left the package manager with only the plugin directory under Go's case-insensitive de-duplication. Startup. A daemon that registered nothing continues when it has just begun installing the profile itself. On a host whose only provider is a dsh without one, failing here is a deadlock rather than a fail-fast: the probe starts the install, the error kills the process milliseconds later, the install's process tree dies with it, and the next start repeats all of it. Nothing else is forgiven. Because such a daemon tracks no workspace, a finished install picks the workspace up before converging. Repair. The reported repair names the missing package and carries no command: the install needs a bundle only the operator can choose, and Repair.Command is rendered to the user inside a shell fence as the line to run, so a placeholder there is a paste-and-fail instruction. Tests. The classification matrix, "manifest present + timeout / non-zero exit / malformed output / future protocol ⇒ no demotion, no provisioning", the 9009 shim, the forced-round sequence, the withdrawal, the startup exception, the empty install, and the case-insensitive PATH merge. Two existing tests read the developer's real ~/.dsh and one fake dsh could not answer `--version` at all — both now pinned to the test's own directory and to the shape the feature is actually about. * feat(daemon): discover the CLI shims DSH Desktop generates The macOS fallback pointed at one path inside the app bundle, and Windows had none at all — every candidate there was a /Applications path that cannot exist, so a Windows host running DSH Desktop reported no dsh whatsoever. Replace it with one layout that serves both platforms. The CLI is not in the install directory. DSH Desktop generates shims under its own per-user data directory — ~/Library/Application Support/DSH Desktop on macOS, %APPDATA%\DSH Desktop on Windows — and on Windows that is roaming application data even though the app installs to Program Files or %LOCALAPPDATA%\Programs. One lookup therefore covers both install modes, since a per-machine install still generates these shims per user. Payload directories are content hashes (and, under host-commands, a hash plus a generation uuid), so they are enumerated rather than composed. Only directories holding a bin/ are accepted, so a stray file or half-extracted download is skipped; modification time orders them so an upgrade's new payload wins over an abandoned one, and the name breaks ties because ReadDir order is not a promise and the daemon must not pick a different CLI on different rounds. host-commands outranks cli, and that is load-bearing: nothing here executes a candidate, so a broken-but-present shim is indistinguishable from a working one at this layer — and on the install that prompted this, the cli shim was exactly that, resolvable by exec.LookPath and exiting 9009. Ranking is the only lever. Generated shims also outrank the macOS app-bundle script, which is entered through `#!/usr/bin/env node` and so only works where node happens to be installed; a shim runs the Desktop app's own runtime by absolute path and needs nothing on PATH. That gap was latent on macOS too. executableCandidate had to learn the platform difference for any of this to work: Go synthesizes a regular file's mode from the read-only attribute and never sets an executable bit on Windows, so the Unix test rejected every candidate and the whole fallback would have been dead code there. Windows uses exec.LookPath, the same check the launcher makes, so a candidate accepted here is one that can actually start — which is also why the Node script has no Windows counterpart and .js never appears in that list. Only default locations are covered, and nothing reads an install receipt or the registry, so a relocated install still needs MULTICA_DSH_PATH. The path builders take goos/env/home as arguments rather than reading runtime.GOOS and os.Getenv, so the Windows layout is asserted from any host — the alternative leaves it testable only on Windows, which in this package has meant untested. * fix(server): refuse a missing DSH runtime profile under its own reason A runtime taken offline for a missing profile reached the server as a bare offline, so runtimeVerdict classified it as waitable/runtime_offline and assignments and @mentions bound to that agent queued forever — even though, with no automatic install configured, that wait never resolves on its own. That contradicts the semantics settled in MUL-6164: when a human has to intervene, refuse the trigger and say how to fix it. Blocked, but not as runtime_unusable. The two causes have opposite repairs: an unrunnable CLI is reinstalled, while a CLI missing its runtime profile is working perfectly and reinstalling it changes nothing. Sharing one code meant every client told DSH users to reinstall a CLI that was never the problem — the chip and toast said "reinstall it there", and the durable system comment left on the issue opened with "its CLI is installed but cannot be executed on that machine", blamed a blocked npm postinstall, and rendered `dsh plugin --profile multica add <bundle>` inside a bash fence as the line to run on that machine. A placeholder presented as a command is the shape of instruction people paste verbatim and then report as broken. So: a new runtime_profile_missing reason code, carried through readiness, dispatch and the localized copy in all four languages. The notice branches on the reason rather than on whether a repair command happened to be present, and the profile text names what is missing, says the CLI is fine, and points at the two ways to supply a real bundle without printing a command to copy. An install the daemon is running right now is the exception: that wait does end by itself, so the work queues instead of being refused. RuntimeBlockedNeedsNotice replaces three inline comparisons, because a code added to one admission path and not the others is a trigger that vanishes with no trace on exactly the surfaces that have no response for the user to read. * docs(runtime): document DSH profile provisioning and shim discovery Automatic provisioning is a new, persistent feature surface, and it had no product surface: an admin had no documented way to configure it, and the one paragraph that existed attached the install semantics to the wrong variable — the sentences describing what MULTICA_DSH_PROFILE_BUNDLE does followed the MULTICA_DSH_PLUGIN_PATH sentence, so "the variable is unset by default and the install it enables is bounded" read as being about a variable that enables no install, and PLUGIN_PATH was then described twice. Environment variables, in all four languages: the install semantics move onto the variable they belong to, and now state the bounds — at most once per daemon lifetime, only on a confirmed-absent profile, failures logged with the package manager's own output and retried only after a restart, a timeout or an unsupported protocol version reported and never installed over. Plus how to diagnose (search the daemon log for `DSH runtime profile`) and how to roll back (delete $DSH_HOME/profiles/multica). MULTICA_DSH_PLUGIN_PATH is described once, and Windows and Linux are named as the platforms with no bundled pnpm directory, where the daemon uses whatever is on PATH unless this points at one. Install guide, in all four languages: what to actually put in the variable. Multica's bridge is not on a public npm registry yet (multica#6936), so a self-hosted team builds it from the repository and points at the build directory, or at the tarball npm pack produces when the daemon host is not the build host; a package name becomes the simplest option once it is published. With a warning that belongs on this value specifically — it is installed into every daemon host's DSH home without further confirmation, so an unvetted third-party package is a supply-chain decision, not a convenience. Also documents where the daemon looks for a Desktop-managed CLI, including the detail that trips people on Windows: it is roaming application data, not the install directory. And the one exception to "the daemon must detect at least one built-in CLI before it will start" — a daemon that has just begun installing the profile itself starts with no runtimes, because otherwise a host whose only tool is a DSH needing that profile could never finish the install. * fix(daemon): find DSH Desktop's bundled pnpm on Windows too The automatic install shells out to `dsh plugin`, which forwards to pnpm, and the daemon inherits no path to the one DSH Desktop ships. The lookup composed a single macOS path and was commented as macOS-only, so on Windows the install failed with "pnpm not found on PATH" unless the operator set MULTICA_DSH_PLUGIN_PATH by hand — on exactly the hosts the automatic install exists for. Both platforms bundle it under runtime-commands in the app's own per-user data directory, but not in the same shape: macOS 2.0.5 keeps a flat runtime-commands/bin, while a Windows install keeps runtime-commands/generations/<id>/bin. Composing either path leaves the other broken, so the lookup now shares the enumerator with CLI-shim discovery and searches both shapes at both levels. A Desktop version that moves between them is then a version this daemon keeps working with rather than one it silently stops supporting. Sorting is pooled per tier rather than per directory. Doing it per directory made the answer depend on the order ReadDir returned DSH profile names, which is the same non-determinism the name tie-break exists to remove: the newest generation has to win even when it belongs to a profile that sorts later. An existing test caught this while the enumerator was being extracted. An explicit MULTICA_DSH_PLUGIN_PATH now replaces the search instead of merely being tried first. An operator who pinned a directory is bypassing the bundled pnpm on purpose, and quietly falling back to it would defeat that. Documented in all four languages, where the previous text stated the opposite — that Windows has no bundled directory. * fix(server): bound the in-flight DSH install claim instead of trusting it `installing` tells the server to queue triggers rather than refuse them, so a claim that outlives its install recreates exactly the failure the structured reason was added to prevent: work queued behind something that already stopped. The withdraw cannot be made to guarantee that. Three paths leave the claim on the row. The verdict reads dshInstallInFlight when it is built, but the wait is recorded later, when the demotion knows the runtime ids — an install that fails in between finds an empty list, and the `installing` written afterwards is never withdrawn. The corrective Deregister is best-effort, and during shutdown it runs on an already-cancelled lifecycle context. And a daemon restarted mid-install does not track the row at all. Bound the claim instead. SetAgentRuntimeOfflineWithReason stamps updated_at, and the install it describes is itself bounded by the daemon's provisioning timeout, so honouring `installing` only inside a window comfortably longer than that covers a real install while capping every way the claim can be left behind. A row with no usable timestamp cannot be bounded and is not honoured: refusing during a real install costs one retry the notice already asks for, while honouring a stale claim costs work that queues until it expires. The withdraw stays as the fast path — this is the floor under it. * refactor(daemon): scrub URL credentials in provisioning output, not globally The rule was added to pkg/redact's shared pattern list, where redact.Text runs over every agent's task messages, comments and error text — so a fix aimed at one package manager's output changed what every provider emits. The gap is real and still worth closing in that list, but widening a shared filter is its own change with its own blast radius, and it does not belong in a PR about DSH. Move it to dshProvisionOutput, which is the only place this PR needs it: a bundle spec can be an authenticated npm URL, and both the package manager and the registry echo the request URL back in their error text. The shared redactor still runs after it for the token shapes it already knows. Behaviour for the provisioning log line is unchanged; behaviour for every other provider returns to main's. Same assertions as before, moved next to the code they now cover, plus the false-positive cases — a port, an scp-style git remote, a bare colon in a path — and one asserting the host survives, since which registry refused the request is what an operator reads and is not the secret. * fix(daemon): let a converge round condemn dsh only convergeRuntimeRegistrations started consuming the verdict map it used to discard, so that a round forced by a DSH profile mismatch could reach demotion — a registered dsh whose profile was removed leaves nothing missing a runtime, so the round has no registration left to do and would otherwise return having done nothing. But it acted on every provider's verdict, not just dsh. On main, demotion is reached from refreshAgentVersions alone, on its own ten-minute cadence, and that is the schedule every provider's verdict was tuned against. Claude Code, Codex and the rest would have moved onto a different one as a side effect of a DSH fix — a behaviour change for providers this PR has no reason to touch. Narrow it to dsh. It is the only built-in with a precondition that can change without any version changing, which is why it is the only one that needs a round it can be forced into; every other provider keeps main's behaviour exactly. |
||
|
|
3dcfdaea41 |
feat(issues): simplify board defaults and sorting (#8256)
* feat(issues): simplify board defaults and sorting Co-authored-by: multica-agent <[email protected]> * fix(issues): address board review feedback Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9a3d92da12 |
MUL-7183: reduce issue search normalization work (#8204)
* perf(search): conditionally normalize issue descriptions Co-authored-by: multica-agent <[email protected]> * test(search): guard conditional description skip Co-authored-by: multica-agent <[email protected]> * refactor(search): align multi-term guards Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
9d0cab0eac |
fix(issues): fail safely when pagination counts are unavailable (#8201)
Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
200dcb44cc |
MUL-7146: lowercase comment content once per search row (#8164)
* perf(search): lowercase comment content once Co-authored-by: multica-agent <[email protected]> * refactor(search): clarify lowered comment projection Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3ac53a68a6 |
MUL-7108 fix(autopilot): judge every autopilot write as the human it acts for (#8107)
Autopilot write endpoints judged the request's authenticated user, which under
a mat_ task token is the RUNTIME OWNER. Any member could therefore @ an agent
running on someone else's machine and spend that machine owner's autopilot
rights — pause a rule, re-point a webhook, rotate a token, or grant themselves
a collaborator row.
Every write on this surface now resolves the human it acts FOR (a member acts
for themselves, an agent for its run's originator), through one seam, and
stamps that same human into created_by / published_by / granted_by and the
rule versions. The gate and the row can no longer name two different people.
Two bypasses of that gate are closed with it:
- The webhook credential was broadcast to the whole workspace on
autopilot:updated, handing every member the token GetAutopilot had just
redacted for them. All five trigger fan-outs now carry a redacted copy; the
HTTP caller still gets the live value.
- Agent identity itself was forgeable: Auth stripped only X-Actor-Source, so a
member could replay an observable (agent_id, task_id) pair — both returned by
GET /api/issues/{id}/task-runs — and be resolved as that agent. Auth and
DaemonAuth now strip both headers, leaving the mat_ branch as the only writer
of agent identity.
That last part is MUL-3428, reported and fixed by @CatCatUncle in #4313; it is
carried here because this change cannot be correct on top of a forgeable actor.
Behavior tightening, no migration: the only path it breaks is a member changing
an autopilot they hold no grant on. A run with no originator can no longer
write here.
Co-authored-by: CatCatUncle <[email protected]>
|
||
|
|
d3f1e64ea8 |
MUL-7055: refactor issue search to candidate-first pipeline (#8041)
* perf(search): use candidate-first issue search Co-authored-by: multica-agent <[email protected]> * fix(search): address candidate-first review feedback Co-authored-by: multica-agent <[email protected]> * fix(search): address review nits Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
70ea4c8f0d |
MUL-7050 perf(search): remove unused exact search counts (#8033)
* perf(search): remove unused exact issue count Co-authored-by: multica-agent <[email protected]> * perf(search): remove unused project count Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
0eff894b59 |
fix(server): preserve legacy handoff notes (#7989)
Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d276991c60 |
MUL-6743 refactor(issues): remove the assignment handoff note (#7626)
* refactor(issues): remove the assignment handoff note (MUL-6743) Usage data puts the handoff note at 0.2% of assignments, so the box occupied the confirm dialog's centre for the other 99.8% and bought them nothing. Removing it takes the whole feature with it: the note input and its runtime version gate, the `handoff_note` write field on PATCH/batch issue updates, the task column write, and the daemon's opening-prompt + issue_context.md handoff sections. The confirm dialog itself stays — it still names who is about to run and still offers "Don't start yet" (suppress_run). The `*WithHandoff` enqueue methods only ever varied by that note, so they become `EnqueueTaskForIssueByActor` / `EnqueueTaskForSquadLeaderByActor` — the actor-carrying variants autopilot already called with an empty note. `preview-trigger` drops `handoff_supported`, which nothing read after the gate moved client-side. `agent_task_queue.handoff_note` is deliberately left in place: it holds historical rows and this is the expand half of an expand-contract pair. Nothing reads or writes it now, so a follow-up migration can drop it once no deployed binary selects it. Co-authored-by: multica-agent <[email protected]> * docs(issues): reshoot the assignment confirm dialog (MUL-6743) The asset still showed the handoff note box the surrounding prose no longer describes. Recaptured from the app at the same 600x450 frame; the confirm button now carries the send-chord keycaps it has shipped with since MUL-5694. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Lambda <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
765321f053 |
MUL-6951 fix(autopilot): carry the trigger creator's authorization on scheduled runs (#7918)
* fix(autopilot): carry the trigger owner's authorization on scheduled runs A schedule/webhook autopilot run left originator_user_id NULL, so canInvokeAgent saw no human and fail-closed against private agents. Arming a trigger is the same authorizing act as clicking "run now" — deferred, not absent — so withholding the originator did not remove that authority. It only forced every capability the automation legitimately needed to arrive as a separately-bounded borrow path: MUL-4857 for child creation, MUL-6691 for the run's own issues, #7902 for the accountable member's issues, with sub-issue dispatch queued up as the fourth. Each carries a hand-written scope that no operator can see in the UI. attribution.TriggerOwner / RuleOwner now populate UserID (the originator) instead of only the accountable side. finalizeAttribution mirrors it, so the originator == accountable invariant that migrations 190/197 enforce holds without a schema change or backfill. Both autopilot execution modes resolve through triggerOwnerAttribution, so run_only and create_issue are covered by the same two lines. This grants exactly the trigger owner's own rights — canInvokeAgent is untouched, only the principal handed to it changes — and originator_source still distinguishes a scheduled fire from a manual one in the audit trail. The borrowed-authority machinery in handler/agent_access.go is now dead for newly enqueued runs but is deliberately left in place: tasks enqueued by the previous backend are still running during a rolling deploy and carry originator NULL, and they must keep the path they were admitted under. Deleting it is a follow-up once this is deployed. Composio connected-app credentials also key on originator_user_id, so scheduled runs now receive the trigger owner's connected apps. Decoupling that from authorization was considered and explicitly deferred. Verification (real PostgreSQL 17): - internal/attribution, internal/service, internal/handler: PASS - the five attribution_stamp tests that pinned "originator stays NULL" were updated to assert the new principal, and fail against the old constructors - pre-existing, unrelated failures in internal/daemon/execenv (4 Hermes store-path tests) reproduce identically on a clean tree Co-authored-by: multica-agent <[email protected]> * refactor(autopilot): delete the borrowed-authority machinery With a scheduled run carrying its trigger owner as originator, every autopilot-shaped authority path in agent_access.go is unreachable: the invoke gate resolves a real human from the speaking task and judges it exactly as it judges that human acting directly. Removed: - autopilotTaskAssignAuthority + autopilotAssignAuthorityFromRequest (MUL-6691) - issueBoundToAutopilotTask, taskRunsAutopilotInWorkspace - autopilotDelegationAuthority and its FromRequest / FromComment resolvers (MUL-4857) - effectiveInvocationAuthorityFromRequest - assignAuthorityScope, assignScopeKind and the four scope constructors - commentTriggerComputeOptions.AutopilotDelegationAuthorityUserID + effectiveInvoker() validateAssigneePair and triggerTasksForComment lose their now-meaningless scope / delegation-authority parameters; every caller passes the run's originator instead. This also retires the fourth borrow scope #7902 proposed. CreateIssue still loads the parent issue when assigning, even though the gate no longer needs the row: it is what turns a cross-workspace or missing parent into 400 "parent issue not found" instead of a 403. Covered by a test. Two behaviour notes, both accepted: - Authority now propagates down a delegation chain, because the parent run finally has an originator to copy. Under the old design each hop had to re-borrow. This matches what a manual "run now" already did. - The old borrow path required task.Status == "running"; the originator path does not, and never did for any non-autopilot run. Task tokens are revoked at the terminal transition (TaskService, MUL-2600), so the primary guard remains; this only drops a second layer that existed solely for autopilot runs. Tests: the two suites that pinned the borrow bounds (autopilot-created-issue binding, authored-by-this-task binding) described a mechanism that no longer exists, so they collapse into one file asserting the rule that replaced them — rights / no rights / no human at all, across create-top-level, create-child, update and batch. The mention-authority suite keeps its shape; its fixture now gives the run a human, as a live run has. Verification (real PostgreSQL 17): internal/attribution, internal/service and internal/handler all PASS. Pre-existing unrelated failures in cmd/multica, internal/cli, internal/daemon and internal/daemon/execenv reproduce identically on a clean tree. Co-authored-by: multica-agent <[email protected]> * fix(autopilot): resolve one principal per dispatch and expire it with the run Addresses Elon's review of #7918. Three findings, all confirmed against the code before fixing. P1-1 — admission and the run's identity could be two different people. Automatic dispatch admitted the assignee agent as the AUTOPILOT CREATOR (canCreatorInvokeAgent) while the task took its originator from the trigger. With A owning both the autopilot and a private agent, and B owning the trigger, a dispatch was admitted as A and then executed as B — rights neither holds alone. The old suite could not see it because it called dispatchRunOnly directly, skipping admission. - autopilot_trigger gains an IMMUTABLE created_by (migration 449, best-effort backfill from published_by). published_by keeps its audit meaning and still transfers on a substantive edit; it is no longer an authorization input, because letting an editor move it silently re-authorizes the automation as that editor. Per Bohan's ruling the run always acts as the trigger CREATOR. - ResolveAutopilotTriggerPrincipal is now the single answer to "who does this run act as". Admission, the task stamp and every delegated run read it, so the fork is unrepresentable. It also binds the trigger to its autopilot (GetAutopilotTriggerForAutopilot — the old lookup was WHERE id = $1, which would let a foreign trigger select the principal) and re-checks workspace membership on every dispatch, so removing a member revokes their triggers. - canCreatorInvokeAgent is now unreachable and deleted. - RuleOwner reverts to accountable-only. It is a guess at who owns a rule; promoting it would hand a legacy trigger somebody's rights without that person arming anything. Such runs carry no originator and fail closed. P1-2 — a terminal task could still spend its human's rights. invokeOriginatorFromRequest now refuses completed / failed / cancelled tasks. Token revocation at the terminal transition stays the primary guard, but it is best-effort, explicitly non-fatal on failure, with a 24h expiry behind it. The check sits at the authorization boundary rather than the auth boundary, so a finishing run can still post its final comment but cannot start new work. P1-3 — the PR and code comments described Composio backwards. BuildTaskOverlay ignores the originator entirely and builds from the AGENT OWNER's connections and allow-list (MUL-3963). Naming the originator hands out no third-party credentials. Comments corrected; no decoupling was ever needed. Operational note: a trigger with neither created_by nor published_by (predating migration 186) resolves no principal, so its autopilot stops dispatching until the trigger is re-saved. That is the intended fail-closed behaviour. Tests: full dispatch through admission for both fork directions; principal fails closed on legacy / cross-autopilot / removed-member; terminal task refused on all three statuses; the edit-transfer tests now assert the principal STAYS with the creator while published_by moves. Verified the fork test fails when admission is reverted to canCreatorInvokeAgent. Verification (real PostgreSQL 17, pristine database): internal/attribution, internal/service, internal/handler and cmd/server all PASS. Migration 449 up/down round-trips. Pre-existing unrelated failures in cmd/multica, internal/cli, internal/daemon and internal/daemon/execenv are unchanged. Co-authored-by: multica-agent <[email protected]> * fix(autopilot): bind the trigger principal to its workspace, correct stale docs Second round of Elon's review on #7918. 1. The trigger lookup only enforced half of what its comment claimed. GetAutopilotTriggerForAutopilot matched on (trigger id, autopilot id); the workspace was used only for the creator's membership check afterwards. That check proves the resolved human belongs to the workspace PASSED IN, not that the autopilot does — a member of two workspaces satisfies it while the trigger came from the other tenant. The query now joins autopilot and filters on workspace_id, and the resolver passes it through. Regression test seeds two workspaces sharing one member and asserts the foreign workspace resolves nobody; verified it fails against the pre-fix query. 2. Several permission-boundary comments still described the pre-MUL-6951 model — that a run's authority and its accountable human transfer to whoever last edited the trigger. That is now the opposite of the code, and these are the comments a future reader would use to reason about the authorization model, so they are corrected at all four sites: the trigger create site, the SetAutopilotTriggerPublisher query, triggerOwnerAttribution, and the dispatchRunOnly attribution block. published_by is described as what it now is: a config-audit column with no bearing on a run. 3. The migration comment now states the backfill's imprecision plainly: for an already-edited trigger it freezes the last recoverable EDITOR as the immutable creator, not the historical creator, which the schema never recorded. It also records that a NULL/NULL trigger has no recovery path by decision (Bohan: leave them empty) — re-saving re-stamps published_by, not created_by — so its runs stay refused with a recorded failure_reason. Not done, per Bohan's ruling: no re-save takeover path for legacy triggers and no migration-period alerting. Verification (real PostgreSQL 17, pristine database): internal/attribution, internal/service, internal/handler and cmd/server PASS; full suite shows only the four pre-existing unrelated failures (cmd/multica, internal/cli, internal/daemon, internal/daemon/execenv). go vet ./internal/... and git diff --check clean. Co-authored-by: multica-agent <[email protected]> * docs(autopilot): carry the creator-principal semantics into every description Third round of Elon's review on #7918. Two must-fixes, no behaviour change. 1. The final semantics had not reached every surface that describes them. Migration 189 wrote COMMENT ON COLUMN text for autopilot_trigger.published_by_* saying it decides the accountable human of the runs a trigger fires. That is now false, and because sqlc generates column comments into models.go, the stalest description of the authorization model sat in generated code every reader passes through. 189 is released and untouched; migration 449 overrides both comments and documents created_by_* for the first time, and sqlc is regenerated. 449's down restores 189's original text verbatim so a rollback does not leave MUL-6951 wording describing a model the database no longer runs. The same stale semantics remained in prose at six code sites — the plain and manual dispatch entry points, the scheduled-plan path, the create_issue enqueue comment, the autopilot-level republish path, and the webhook trigger create site. All said a run's authority or accountable human follows the last editor, or that automation resolves rule_owner. These sit on the authorization path, so they are corrected rather than left to drift. published_by is now described as what it is: config audit, with no bearing on a run. Also corrects the backfill comment's migration reference: trigger publishers arrived in 189, not 186. 2. The test file this PR added open-coded INSERT ... RETURNING id with matching t.Cleanup(DELETE ...), which CLAUDE.md prohibits. Rewritten onto testutil.Fixture: a small principalFixture embeds it and every row — members, runtimes, private agents, autopilots, triggers, the second workspace — is built and cleaned up by dbfx. The only remaining DELETE is the mid-test member removal that IS the behaviour under test. Re-verified after the rewrite that both regressions still fail against the old code: reverting admission to the autopilot-creator check reddens the fork test, and dropping the workspace join reddens the cross-workspace test. Verification (real PostgreSQL 17, pristine database): full suite shows only the four pre-existing unrelated failures (cmd/multica, internal/cli, internal/daemon, internal/daemon/execenv). Migration 449 up/down round-trips and the down restores 189's comments. go vet ./internal/... and git diff --check clean. Co-authored-by: multica-agent <[email protected]> * docs(autopilot): state the backfill widening plainly, drop raw-SQL test seeding Fourth round of Elon's review on #7918. Both items are leftovers from the previous round; no behaviour change. 1. Migration 449's backfill comment carried two factual errors. It said published_by_* arrived in migration 186; it was 189. The previous round intended to fix this but the string replacement did not match the wrapped line and silently did nothing, so the wrong number shipped. It also claimed the backfill "is no wider than the authority those runs carry today", which is false: before MUL-6951 an automatic run had no originator at all, only narrowly-scoped borrow paths, so promoting a backfilled editor to the run's full originator IS a widening. The product decision to accept it stands; the comment now says so directly as a compatibility tradeoff instead of denying the widening. A migration comment that understates what it grants is worse than no comment. 2. The new test file still reached raw SQL transitively. It called seedAttributionFixture, which hand-writes INSERT and cleanup for its user / workspace / member / runtime / agent — so the file's own claim that "every row goes through testutil.Fixture" was untrue and the repository's fixture rule was still unmet. It now bootstraps the base identity with Fixture.User / Workspace / Member on an unbound fixture and adopts it, which also drops the agent and issue that helper builds and these tests never use. The comment explains why the helper is deliberately not reused. Re-verified after the fixture change that both regressions still fail against the old code: reverting admission to the autopilot-creator check reddens the fork test, and dropping the workspace join reddens the cross-workspace test. Verification (real PostgreSQL 17, pristine database): full suite shows only the four pre-existing unrelated failures (cmd/multica, internal/cli, internal/daemon, internal/daemon/execenv). go vet ./internal/..., gofmt and git diff --check clean. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
835c5437a9 |
MUL-6971 fix(issues): stop rejecting description edits against a stale baseline (#7926)
* fix(issues): stop rejecting description edits against a stale baseline The description editor autosaves on a 1.5s debounce and sent the base it had adopted; the server rejected the write whenever that base was not byte-identical to the stored Markdown. Two ways that fired with no second writer present: - The editor normalizes its base (`normalizeMarkdown` = trim) while the server compares raw bytes, so any description stored with surrounding whitespace — every CLI/integration write that keeps a trailing newline — rejected EVERY edit, permanently. - A save whose own echo arrived while the editor was dirty could not advance the base (ContentEditor refuses external syncs on a dirty document), so the next autosave carried a base the server had already moved past. Entering the conflict state then fed the local draft back in as the base, wedging the editor for the rest of the session. The guard also never covered the writers most likely to race a human here: mobile omits text baselines deliberately, and the CLI/agent path sends no base at all. It mostly rejected the user's own autosave. Descriptions are now last-write-wins. `description_base` stays in the request because it is also the channel-media merge metadata — dropping it would make media the editor had adopted impossible to delete. Title and comment baselines are untouched: both save explicitly, send an un-normalized base, and are not reachable by either failure above. Co-authored-by: multica-agent <[email protected]> * test(issues): build the new description-baseline test on the shared fixtures The test landed on the legacy pattern this package is trying to stop growing: `insertWorkflowTestIssue` plus a hand-written cleanup, raw SQL to seed the column under test, and a recorder/status quartet. Rebuild it on `dbfx.Issue` and `testutil.Call(...).Want(...)`, which own the cleanup and print the request line and response body on a mismatch. Behaviour is unchanged — still red against the pre-fix handler with the reported 409, green after it. The assertions added to the surrounding legacy test stay in their host's style; the rule covers new tests. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
eddb0dc2ad |
MUL-6884 feat(metrics): attribute agent_runtime lookups by product source (#7822)
* feat(metrics): attribute agent_runtime lookups by product source
MUL-6884 asked which product API is behind the hot GetAgentRuntime query.
It has no single answer: a dozen unrelated paths issue the same primary-key
read, so pg_stat_statements sums them into one row and can report that the
statement is busy without saying which behaviour is driving it. Deciding
between "raise the heartbeat interval" and "back off the 500ms browser polls"
needs that split, and today it is a guess.
Add multica_agent_runtime_lookup_total{source,result} and route every
production read through service.RuntimeLookup, which carries the source with
the connection and classifies pgx.ErrNoRows as not_found rather than as an
error — a deleted runtime is the daemon's self-heal signal, not an outage.
The labels are a closed enum (15 sources x 3 results, all prewarmed at zero)
because this fires on the hottest read in the system; an unclassified call
site degrades to "other" instead of minting a series. No query, no timing
histogram, and no extra SQL on /metrics — this PR only makes the existing
calls countable.
An AST lint test fails CI on any direct Queries.GetAgentRuntime call outside
the wrapper, since a bypassed call site would quietly undercount whichever
source it belonged to and send the follow-up work after the wrong caller.
Co-authored-by: multica-agent <[email protected]>
* refactor(metrics): drop the standalone handler lookup file
The two helpers it held are not an endpoint — no route, no HTTP handler, no
new file under cmd/ — but a new agent_runtime_lookup.go under internal/handler
reads like one, and a file name that has to be opened before it can be ruled
out is a bad file name.
Move both into runtime.go next to requireRuntimeReadAccess, where the rest of
the runtime read helpers already live, and name the metrics test after what it
tests. No behaviour change.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
|
||
|
|
db86c75e12 |
MUL-6878: avoid per-row issue status resolution (#7814)
* perf(server): avoid per-row issue status resolution Co-authored-by: multica-agent <[email protected]> * fix(server): address issue status query review nits Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
d6ecf4bc8a |
fix(issues): derive a usable status key from non-ASCII display names (MUL-6749) (#7638)
Creating a custom issue status whose display name is written entirely in a
non-Latin script — 客户确认, 고객 확인, 確認待ち — failed with "cannot derive a
status key from that name; provide one explicitly". The key alphabet is ASCII,
so such a name slugified to nothing, and the settings form exposes no field for
an explicit key: there was no way to create the status through the UI at all.
The key stays an ASCII machine handle — it is what issue.status stores, what the
CLI takes as an argument, and what the column CHECK permits — so there is NO
migration. When the name has nothing to slug the key falls back to the status's
CATEGORY plus an ordinal (in_review_2), which keeps saying which platform
behavior the status inherits.
Also fixed in the same path:
- Two names that collapse onto one slug ("待客户 Review" and "待供应商 Review"
both slug to "review") no longer make the second create fail with a conflict
blaming a display name nobody took. Disambiguation counts ARCHIVED rows, since
idx_issue_status_workspace_key is not partial.
- Every create takes the exclusive catalog lock, not just the deriving one, so
an explicit-key insert cannot land between a derive's catalog read and its
insert and hand a 409 to a form with no key field.
- The ordinal scan is bounded by the catalog rather than a constant.
A derived key is opaque, so the places that printed a key alone now carry the
name: issue payloads gain status_name (HTTP and background events alike, from
one catalog read), the unknown-status 400 lists `key (Name)`, creating a status
surfaces the key it minted, and `multica issue status --help` says the argument
is a key rather than a display name.
Fixes #7627. Supersedes #7636.
|
||
|
|
d13e76a557 |
fix(billing): prompt upgrade before quick create (MUL-6746) (#7625)
* fix(billing): prompt upgrade before quick create * fix(billing): use public core export * fix(billing): keep issue-limit recovery actionable * test(handler): provide retry workspace context * fix(billing): respect issue-limit prompt dismissal * fix(billing): clarify issue-limit recovery paths |
||
|
|
5fa65bd125 |
feat(billing): enforce Cloud issue count limits (#7619)
* feat(billing): enforce Cloud issue count limits * fix(billing): address issue limit review feedback * test(billing): exercise validation as manager * fix(billing): resolve second review feedback * fix(core): preserve entitlement limit discrimination |
||
|
|
7b8c783991 | fix(issues): let verified autopilot runs assign private agents (#7572) | ||
|
|
3d47913c85 |
fix(issues): allow verified autopilot child assignment (MUL-6161) (#7275)
* fix(issues): allow verified autopilot child assignment * test(issues): cover autopilot private squad assignment * test(issues): harden autopilot assignment coverage |
||
|
|
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 |
||
|
|
1dc135598e |
MUL-6343: Enforce recently created issue windows (#7305)
* MUL-6343: enforce recently created issue windows Co-authored-by: multica-agent <[email protected]> * fix(issues): address creation window review Co-authored-by: multica-agent <[email protected]> * fix(issues): bound table window ID cache Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
c2474e2fe1 |
fix(agents): align private-agent invoke surfaces with the owner-only contract (MUL-6380) (#7202)
* fix(mobile): gate the chat agent picker on the real invoke rule (MUL-6380) apps/mobile/lib/can-assign-agent.ts was a hand-copied mirror of the pre-MUL-3963 permission model and had drifted from the server in both directions: - it granted workspace owner/admin a bypass on private agents, so an admin could pick a teammate's personal agent in the chat picker and only learn it was refused when the send hit canInvokeAgent (403); - it judged on the derived `visibility` field, which collapses "public_to scoped to specific people" into "private" — so a member the owner had explicitly granted access to never saw that agent on mobile at all. Delete the mirror and call `canAssignAgentToIssue` from @multica/core/permissions directly, the same rule the issue mention bar on this app already uses (mention-suggestion-bar.tsx). The root CLAUDE.md sharing rule allows mobile to import pure functions from @multica/core, so the mirror bought nothing but a second copy to drift. No new tests: the access matrix stays canonical in packages/core/permissions/rules.test.ts, which already covers the admin, owner, plain-member and member-target cases. Co-authored-by: multica-agent <[email protected]> * fix(agents): stop telling users workspace admins can run a personal agent (MUL-6380) Since MUL-3963 the invoke gate is owner-only for a private agent — admins keep management and view access, not the ability to trigger a run. Several strings still describe the old model, and they sit next to controls that enforce the new one: the "Personal" badge tooltip on the agent detail page, overview summary and hover card says "only you and workspace admins can use this agent", while the assignee picker beside it is disabled and reads "only the owner can assign work". - packages/core/agents/visibility-label.ts: fix VISIBILITY_DESCRIPTION and VISIBILITY_TOOLTIP (shown in the create-agent dialog, the visibility picker and the agents-list lock tooltip). The description covers a CHOICE, which submits permission_mode "private" — owner-only, so "Only you can assign". The tooltip covers an EXISTING agent, where `visibility: "private"` is the lossy projection that also covers "shared with specific people" — so it names the owner's grants instead of promising either extreme. The doc comments asserted the opposite contract and cited a long-stale line ref. - packages/views/locales/{en,zh-Hans,ja,ko}/agents.json: same fix for the VisibilityBadge tooltip, which is also its aria-label. Co-authored-by: multica-agent <[email protected]> * fix(issues): stop naming the target's permission mode when an assign is denied (MUL-6380) validateAssigneePair answered a refused assignment with "cannot assign to private agent" / "cannot assign to squad with private leader". Both are wrong twice over: - they disclose the target's permission mode to a caller who was just told they may not use it, which is exactly what ReasonInvocationNotAllowed (dispatch/reason.go) deliberately avoids — it does not distinguish "not permitted" from "does not exist"; - "private agent" is simply inaccurate for a `public_to` agent scoped to specific people, which is refused through the same branch. Report the missing permission instead of the target's configuration. Co-authored-by: multica-agent <[email protected]> * fix(issues): correct the authorization contract stated at the assignment gate (MUL-6380) Review found the drift this PR set out to fix still present in the comments and test docs closest to the gate itself: - validateAssigneePair's doc comment claimed assignment runs canAccessPrivateAgent and that agent callers bypass the gate. It runs canInvokeAgent, and an agent caller is judged by the top-of-chain human originator like anyone else — the code twenty lines below has said so since MUL-3963. - TestShouldEnqueueOnComment_PrivateAgentGate's doc listed "allow workspace owners/admins" and "allow agent-to-agent traffic regardless of visibility"; its own cases assert both are DENIED. - TestCreateIssue_SquadPrivateLeader_OwnerAllowed's doc said a workspace owner can assign; the body uses the leader agent's owner, and an inline comment already corrected the mismatch. - The comment added earlier in this PR overclaimed: it read as if the 403 were enumeration-safe, but the not-in-workspace branch still answers 400. Narrowed to the guarantee actually made — the reason no longer names the permission mode. Also pins the denial BODY, which no test covered: status-only assertions let the reason sentence drift back into describing the target. New cases cover the workspace owner, a plain member, an agent actor with no originator, a `public_to` agent scoped to someone else (the case the old wording called "private", which was simply false), and a private-leader squad. Co-authored-by: multica-agent <[email protected]> * fix(chat): make a session read-only when its agent can no longer be run (MUL-6380) Filtering the agent PICKER was only half the path. A chat session outlives the permission that created it: the agent can be flipped to personal, change owner, or drop the member from its allow-list. The server already treats that as a defined state — the transcript stays readable through the view gate while every send is refused by the invoke gate (MUL-4525, admission_security_mul4525_test.go) — but the clients kept resolving the open session's agent from the full list and never re-checked whether it could still be run. The composer looked live, and the user found out from a 403. Judge the SESSION's agent, not just the picker list, on every surface: - packages/views: `isAgentAccessRevoked` in use-chat-controller.ts and chat-window.tsx, wired into the composer disable, upload affordance, quick actions, queued send-now, and a new AgentAccessRevokedBanner. handleSend keeps a belt-and-braces guard so it never spends a roundtrip on a certain refusal. - apps/mobile: same derivation in the chat tab, plus the failure path — a thrown send only restored the draft, so the 403 read as a silent no-op. It now names the cause, via a mobile-local `dispatchReasonCode` (core's narrows on core's ApiError class, which mobile never throws). Two reason-precedence fixes fall out of this. "No agents in this workspace" was shown when the workspace's ONLY agent was the revoked one — an agent IS bound, it just cannot be run — so revoked access now wins in the placeholder, the banner and mobile's disabled reason. And the queue's Steer button said "Steer is available after the current reply starts", telling the user to wait for something waiting cannot fix; it now states the permission reason. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
5d5fe64672 |
MUL-6343: Add semantic issue activity timestamps (#7193)
* feat: add semantic issue activity timestamps Co-authored-by: multica-agent <[email protected]> * fix(migrations): register issue activity index cleanup Co-authored-by: multica-agent <[email protected]> * fix(issues): address last activity review Co-authored-by: multica-agent <[email protected]> * fix(issues): close last activity review gaps Co-authored-by: multica-agent <[email protected]> * fix(issues): harden activity rollout safeguards Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
3460f9aeaf |
MUL-6342: enforce entitlement-backed autopilot quotas (#7194)
* feat(autopilot): enforce entitlement run quotas Co-authored-by: multica-agent <[email protected]> * chore(deploy): expose entitlement policy config (#7195) Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(autopilot): address quota review findings Co-authored-by: multica-agent <[email protected]> * fix(autopilot): finish quota review nits Co-authored-by: multica-agent <[email protected]> * refactor(autopilot): simplify quota schema Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
4b8873df3a |
feat(issues): add revision-aware concurrency guards (#7038)
* feat(issues): add revision-aware concurrency guards * fix(concurrency): decouple text edits from aggregate revisions |
||
|
|
14c2e4e831 |
MUL-6243 feat(issue-status): board fetches by category; archive retires without migration (#7084)
* feat(issue-status): board fetches by category; archive retires without migration (MUL-6243) Product decisions from the issue thread: 1. Board fetches by CATEGORY, not per status. PAGINATED_STATUSES becomes PAGINATED_CATEGORIES and the cache buckets by category, so the board's fan-out stays fixed at 7 parallel requests however many custom statuses a workspace defines — instead of one more request per status added. Adds status_category / status_categories filters to the issue list endpoints, backed by the existing issue_effective_status() SQL function. 3. Archiving retires a status from FUTURE use only. It no longer refuses while issues still carry it: those issues keep their status and keep resolving to their category's behavior (Effective ignores archived_at), while Resolve still rejects the status so nothing new can be assigned to it. The exclusive catalog lock stays — it is what makes "no NEW assignment after archive" exact rather than approximate. Also lands the frontend catalog foundation: packages/core/issue-statuses with useIssueStatuses(wsId) (React Query, keyed by wsId per the state rules) and a pure buildIssueStatusCatalog for non-React callers, plus IssueStatusCategory as a distinct type from the status key and status_category on issue payloads. NOT included, and deliberately: widening IssueStatus to string. That change forces every Record<IssueStatus, …> presentation lookup in packages/views to resolve a key to its category first, and it is the first task of the follow-up PR. Everything here is the groundwork that makes it mechanical. Co-authored-by: multica-agent <[email protected]> * fix(issue-status): restore the status index, make category authoritative, keep archived resolvable Review round 1 on #7084 (MUL-6243): 1. Category filtering wrapped issue.status in issue_effective_status(), which makes the (workspace_id, status) index unusable — and the board switched to it unconditionally, so every workspace paid for it immediately. Categories are now expanded to concrete status keys (issuestatus.ExpandCategories) and filtered with an indexed status = ANY(...). Measured on a 100k-row table with the production index: seq scan cost 27013 -> index-only scan cost 6.07. A workspace with no custom statuses expands each category to exactly its own key, reproducing the pre-feature query. Archived keys are included, since issues left on them still belong in their category's column. 2. status_category was only filled for built-ins, so a custom-status issue arriving over the websocket was silently dropped by addIssueToBuckets and a cross-category update was skipped by the cache coordinator, drifting off-window totals permanently. List, grouped, get, create, update and batch responses now carry an authoritative category via a per-request Resolver (archived included), and the client invalidates instead of no-oping when a category is unresolvable. 3. The catalog was fetched without archived entries, so after archiving a status the issues still on it lost their name and colour and had their category guessed as todo — contradicting the decision that archiving leaves existing issues untouched. Resolution now includes archived entries; activeStatuses / inCategory expose only assignable ones to pickers. Co-authored-by: multica-agent <[email protected]> * fix(issue-status): category on every payload exit, one resolver per request, no same-category duplication Review round 2 on #7084 (MUL-6243): 1. Category fill was incomplete and N+1. The create WEBSOCKET payload was unfilled — only the creator's HTTP response was, which is too late for the other tabs the event exists to serve. GetIssue, the open_only list, search, the duplicate-conflict response, the table rows, the GitHub PR update and the onboarding shim were also unfilled. Grouped and batch called the single-response helper per row, so each custom-status row hit the catalog separately. Every exit now goes through newStatusCategoryFiller, a request-scoped closure over ONE Resolver: built-in statuses still cost no query, and a page of custom-status rows costs one catalog read rather than one per row. 2. A status KEY change within one category duplicated the card and inflated the total. patchIssueInBuckets compared patch.status (a key) against loc.status (a bucket CATEGORY), so in_review -> a custom human_review took the cross-bucket path, then deleted from and re-inserted into the same bucket from a pre-delete snapshot. It now resolves the destination category first and compares category to category; same-category changes replace in place and leave the total alone. The destination category is resolved from the patch alone — the merged issue inherits the previous status_category, which would have kept a genuinely moved card in its old column. patchNeedsInvalidation lets the loaded branch of the cache coordinator invalidate instead of silently no-oping on a status it cannot resolve. Also drops the unrelated share_link.sql.go churn from an sqlc version difference, per the review nit. The four cache cases were verified to fail against the previous logic (card duplicated, total +1) before the fix was restored. Co-authored-by: multica-agent <[email protected]> * fix(issue-status): normalize status_category on every cache write; one filler per create Review round 3 on #7084 (MUL-6243): Must-fix — a cached entity kept the STALE status_category after a status change. The server now sends a category on every issue, so a cached entity is {status: "todo", status_category: "todo"}; an optimistic patch carries only {status: "done"}, and a bare {...issue, ...patch} moved the card to the done bucket while the entity still claimed todo. A single update self-heals from the server response, but the batch API returns only {updated: n} and does not refetch bucketed lists, so the inconsistency was permanent — and the next off-window count then decremented the wrong bucket, recreating total drift. normalizeStatusPatch now rewrites status_category to match status before any cache write, applied at applyIssueChange's entry (covering the flat, table and detail spreads), in patchIssueInBuckets, and in the children mutation. An unresolvable custom key drops the inherited category rather than keeping it; patchNeedsInvalidation still routes it to a refetch. Test fixtures now carry status_category for built-ins, matching what the server actually sends — without that the regression was invisible. Non-blocking items also addressed: - CreateIssue builds one filler and shares it between the broadcast payload and the HTTP response, so a custom-status create reads the catalog once. - Background events via IssueToMap now resolve an authoritative category (IssueToMapWithCategory) for the task, issue and autopilot publishers. The channel router keeps plain IssueToMap and says why: it always creates with the built-in "todo", where key IS category. The "one resolver serves a whole page" assertion was REMOVED rather than kept: pg_stat counters lag, so a delta around one request passes even with a per-row filler. Verified that directly before dropping it. The comment now states what is and is not pinned instead of implying coverage that does not exist. Co-authored-by: multica-agent <[email protected]> * fix(issue-status): address review nits - Normalize the single-update children optimistic patch too, matching the batch path. It self-healed on onSettled, but it broke the entity/bucket invariant for the duration of the request. - Rename TestListEndpointsCarryStatusCategory: the old name claimed it covered "without per row queries", which the test explicitly does not assert. - Drop the unrelated gofmt churn in empty_claim_cache.go. - Add a background-event assertion: IssueToMap leaves a custom status uncategorized, IssueToMapWithCategory resolves it — the reason the publishers use the latter. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
446080bd57 |
MUL-6243 feat(issue-status): per-workspace custom issue statuses over 7 canonical categories (#7065)
* feat(issue-status): per-workspace custom issue statuses over 7 canonical categories (MUL-6243) Co-authored-by: multica-agent <[email protected]> * fix(issue-status): address review — normalize all status consumers, close archive race, gate rollout, split PK index Review round 1 (MUL-6243 / PR #7065): 1. Behavior normalization was incomplete. Adds issue_effective_status() as a SQL mirror of issuestatus.Effective so the six SQL consumers the Go resolver cannot reach (duplicate guard, open-issue list, sub-issue progress, project stats, completed-inbox archive, open_only) resolve categories too. Wires the resolver into the task-failure sweeper and the stage-barrier helpers. 2. Archive TOCTOU. Archive now runs its in-use census and the archive itself in one transaction under an exclusive catalog lock; writes that target a custom status take the shared side. Built-in writes stay lock-free — a built-in can never be archived. 3. Rollout safety. Creating a custom status is gated behind the default-off CustomIssueStatuses flag, so a value old pods cannot interpret cannot exist until the fleet is updated. Readers ship ungated and are safe. 4. Primary key index. Follows the repo convention (327-329): table without a PK, unique index built CONCURRENTLY in its own migration, attached via PRIMARY KEY USING INDEX, and registered for invalid-index cleanup. 5. Status normalization vs stored value. Write paths now persist the canonical key from Resolve, so 'HUMAN_REVIEW' no longer validates and then fails the column format constraint as a 500. Also fixes the two CLI tests failing in CI: they asserted the pre-MUL-6243 contract that an unknown-but-well-formed status is rejected locally. Co-authored-by: multica-agent <[email protected]> * fix(issue-status): close the archive race in both orderings; resolve categories in batch barrier and CLI progress Review round 2 (MUL-6243 / PR #7065): 1. The archive race was only half closed. Handlers resolved the status BEFORE opening a transaction, so an archive could commit in that window and the write would still land on an archived key. assertIssueStatusStillActive now takes the shared catalog lock and RE-RESOLVES inside the caller's transaction, which is what actually makes the status provably active at write time. Applied to create, update, batch, and both description-merge branches, which previously bypassed the guard entirely. A losing writer gets 409 rather than a silent partial update. 2. The batch stage-barrier prefilter still compared literals, so a batch that moved the last child onto a custom done status left childDoneCompleted empty and skipped the parent notification. It now resolves categories first. 3. multica issue children --output json counted only literal done/cancelled. ListChildIssues now emits status_category and the CLI reads it, falling back to status for older backends. TestArchiveCommitsInsideTheWriteRaceWindow interleaves the two operations so the writer validates against an active status and the archive commits before the write. Verified it fails without the re-resolve (issue stranded on the archived status) and passes with it. Co-authored-by: multica-agent <[email protected]> * refactor(issue-status): address review nits — honest race-test naming, one catalog read per list Review round 3 nits (MUL-6243 / PR #7065): 1. TestArchiveRaceBothOrderings claimed more than it proved. Its 'archive wins' cases archived first and only then sent the request, which the pre-flight check alone rejects. Split into TestWritesRejectAnArchivedStatus (the sequential case, named for what it is) and TestArchiveRefusesAfterACommitted Write, and extended TestArchiveCommitsInsideTheWriteRaceWindow — the only test that truly interleaves archive between pre-flight and write — from create alone to all five write endpoints. Re-verified by deleting the in-transaction re-resolve: 4 of 5 fail with issues stranded on the archived status (create is guarded in the service layer and checked separately). 2. ListChildIssues / ListChildrenByParents issued one category lookup per custom-status child. issuestatus.Resolver now amortizes that to a single catalog read per request, lazily on the first custom key, so built-in statuses still cost zero queries however long the list is. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
b30a3ae55c |
MUL-6164 feat(runtime): take an unusable agent CLI offline and refuse its triggers with the fix (#6966)
* fix(daemon): take a runtime offline when its CLI cannot be executed (MUL-6164) A CLI that cannot exec was classified as a transient probe failure, so the runtime stayed online and every task it claimed died at fork/exec. The user in MUL-6164 hit the same error once per task with no signal that the runtime was the problem — an npm placeholder stub passes exec.LookPath, so nothing else the daemon checks can tell it apart from a working install. ENOEXEC (and its Windows spelling, ERROR_BAD_EXE_FORMAT) is a fact about the bytes on disk, not a hiccup: it will reproduce until someone reinstalls. Give it its own probe verdict and route it down the track below-minimum already uses — claim barrier, seq-stamped hold, deregistration — so the provider goes offline with its reason instead of collecting failed tasks. Recovery needs no new machinery: a demoted provider has no runtime, which puts it back in providersMissingRuntimes, and the first healthy probe clears the hold and lets converge register it again, with no restart. One thing the below-minimum track did not need: a confirmation window. The repair we tell users to run overwrites the bin entry in place, so a probe landing mid-copy sees a truncated file. Demotion therefore requires two sightings a window apart; a genuinely broken install pays one extra probe round, and a spurious verdict would in any case recover on its own. detectBuiltinRuntimes' second return value is now "providers condemned this round, with the evidence against them" rather than "below-minimum versions" — same shape, both kinds, and the value is the reason /health already shows. Co-authored-by: multica-agent <[email protected]> * feat(runtime): refuse triggers for an unusable agent CLI instead of queueing them silently (MUL-6164) Taking the runtime offline stopped the per-task failures, but it replaced them with silence: the next @mention or assignment queued as if the machine were merely asleep, and nothing ran for two hours until queued_expired. Waiting only makes sense when waiting is a plan — a sleeping laptop comes back on its own; a CLI that cannot be executed does not. dispatch/reason.go already draws exactly that line between runtime_offline ("bring the machine back, queued work waits") and agent_runtime_required ("nothing will ever claim this"), and warns that collapsing the two sends users to fix the wrong thing. This adds the third member of that family, runtime_unusable, and routes the new verdict to it end to end: - The daemon reports WHY on deregistration — a stable code plus the repair command read from the package's own manifest, since only that side knows the path. Stored on agent_runtime.metadata, which registration overwrites wholesale, so a repaired runtime drops the explanation structurally. - AgentReadiness now returns a verdict (available / waitable / blocked) instead of a bool plus prose, and the direct-agent trigger paths start consulting it — blocking ONLY on the blocked verdict, so an offline machine still queues. agentReadinessReasonCode, which re-derived the same code from agent state, is gone: one source, no drift. - Every path that refuses without a human watching the response leaves the reason on the issue: an assignment, or another agent's @mention. The interactive surfaces already carry the code back to the composer, which now localizes it in all four languages. Below-minimum keeps today's behaviour deliberately. It is equally futile to wait on, but changing when THAT refuses a trigger is a separate product decision. Co-authored-by: multica-agent <[email protected]> * fix(runtime): keep the unusable verdict through a late register, and repair Windows (MUL-6164 review) Two ways the refusal path failed to reach the user. The verdict could be erased by an older answer. A register whose request was still in flight across the demotion upserts the runtime row afterwards, and that upsert overwrites metadata wholesale — including the reason just stored. The local hold refuses the response correctly, but the cleanup that took the revived row offline again sent no reason, so the server ended up "offline, cause unknown". Every admission path reads that as "the machine will come back" and queues, which is exactly the behaviour this change exists to remove. The demotion record now keeps the structured cause and the revived-row cleanup re-attaches it per row. The regression test drives the real interleave — a register held inside the server handler across the verdict — and fails without the fix. The repair command was POSIX-only. Windows had just gained detection (ERROR_BAD_EXE_FORMAT), so a Windows user was correctly refused and then handed `cd 'C:\...' && node install.cjs`: cmd.exe treats the quotes literally and PowerShell 5.1 has no `&&`. The command is now rendered for the shell of the machine that reported it — Set-Location on two lines for PowerShell, with PowerShell's own quote escaping — and carries that shell so the notice fences it truthfully instead of labelling everything bash. Also corrected the notice's own docs: it is written for every author, not only unattended ones. The chip and toast carry the reason code but not the repair command, so a human needs the durable copy too. The behaviour was already this; the comment claimed otherwise. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Bohan-J <[email protected]> Co-authored-by: multica-agent <[email protected]> |
||
|
|
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]> |