140 Commits
Author SHA1 Message Date
cfc165ce6a MUL-8095: fix(execenv): normalize required metadata for Codex user skills (#9346)
* fix(execenv): normalize required metadata for Codex user skills

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

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

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

MUL-8095

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

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

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

MUL-8095

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

---------

Co-authored-by: JC的AI分身 <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-10-20 14:55:38 +08:00
e909e9c89d MUL-7587 fix(daemon): show on-behalf-of identity in agent context (#8688)
* fix(daemon): expose original requester in agent context

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

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

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

---------

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Pinned by TestLegacyRedirectsFollowTheDaemonsBrief.

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

---------

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

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

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

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

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

Addresses review on #7972.

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

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

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

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

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

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

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

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

Comment text only.

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-09-04 13:51:12 +08:00
49098deb6f MUL-6984 refactor(prompt): give each duplicated instruction one owner (#7971)
* refactor(prompt): give each duplicated instruction one owner (MUL-6984)

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

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

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

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

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

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

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

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

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

Addresses review on #7971.

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-31 15:27:30 +08:00
mikyan fe59262188 feat(agent): add Huawei Cloud CodeArts support (#6985)
* feat(agent): add Huawei Cloud CodeArts support

* fix(migrations): resolve CodeArts prefix collision

* fix(agent): address CodeArts review feedback

* fix(agent): tighten CodeArts integration scope

* fix(agent): resolve CodeArts follow-up review blockers

* fix(agent): redact CodeArts launch arguments

* docs: sync supported runtime counts

* fix(agent): own CodeArts model probe process tree
2026-08-31 12:30:59 +08:00
cd169947b5 MUL-6686 feat(daemon): use readable workdir paths (#7316)
Create new task environments under sanitized, bounded readable segments:
<workspace-slug>-<workspace-short>/<issue-key>-<task-short>/workdir.

The readable name is the visible part; the substance is moving task identity
off the directory name. A human-readable segment is mutable — workspace slugs
and issue prefixes change — so workspace/task identity now lives in
.task_owner, .gc_meta.json and a stable ID-keyed index under .task_roots that
freezes one physical root per task for its lifetime. GC and disk-usage read
that authoritative identity instead of inferring a workspace from a path
string, which also fixes active and preparation-crashed roots being attributed
to a nonexistent workspace.

Existing workdirs are not renamed or migrated, legacy roots are adopted into
the index, and both layouts render privacy-safe relative paths. Labels are
lowercased ASCII with separators and traversal collapsed, segments are capped
at 24 characters so the two together stay within the previous 49-character
Windows path budget, and stable ID suffixes drawn from the UUIDv7 random tail
keep roots distinct across renames and case-insensitive filesystems.

Closes #6063

Co-authored-by: iiwish <[email protected]>
Co-authored-by: J <[email protected]>
2026-08-26 16:34:10 +08:00
520359a2c9 MUL-6518 fix(daemon): reclaim orphaned per-task temp dirs in the GC (#7391)
* refactor(daemon): give the lock and dir-stat helpers neutral names

openEnvRootLockFile, releaseEnvRootLock and codexStoreStat are all
generic: they take a path, and nothing in them knows about env roots or
Codex stores. The next commit reuses the same execution lock for per-task
temp directories, where the old names would read as a bug at the call
site.

No behaviour change.

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

* fix(daemon): reclaim orphaned per-task temp dirs in the GC

Every task gets a private temp dir (multica-task-*) under the system temp
base, exported to the agent as TMPDIR/TMP/TEMP. Its only exit was the
RemoveAll deferred at the end of runTask, which does not run when the
daemon is killed and does not succeed while a file inside is still open —
the Windows case reported in #7364, where one machine had accumulated 174
leftover directories. The GC never covered them: it scans WorkspacesRoot,
and the temp base is a different root, so multica-task- had exactly one
create site and zero reclaim sites.

Sweep the temp base as a fourth out-of-root pruner. Liveness is decided by
the directory's .task_lock — the same OS advisory lock an env root uses —
not by age: the sweep removes a directory only once it has itself acquired
the lock the owner would still be holding, which the kernel grants only
after that owner is gone.

Using the lock rather than a marker or a TTL is what makes this correct
rather than probabilistic. It is held by an open file description, so it
answers for a live task in this very process as well as one owned by a
different daemon sharing /tmp, which no in-memory active set could see.
And it needs no upper bound on task runtime — there is none to assume,
since DefaultAgentTimeout is 0.

Directories left by daemons predating the lock carry no lock file, so age
is the only signal available for them; they are reclaimed after
MULTICA_GC_TASK_TEMP_LEGACY_TTL (72h). That branch exists to clear the
existing leftovers and can be dropped in a release or two.

The end-of-run RemoveAll stays exactly as it was, and nothing was added to
the task-completion path: the run releases the lock before removing, so a
removal that fails there costs nothing and the next GC cycle finishes the
job.

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

* fix(daemon): keep the temp dir marker when cleanup cannot finish

Two fixes from review.

1. A failed cleanup destroyed the marker the next sweep needs.

   os.RemoveAll keeps walking after a failure, so removing a directory
   whose payload is still open deleted the .task_lock sitting next to it
   and then returned the error. The directory came back as one with no
   marker — indistinguishable from a pre-lock leftover, so it was no
   longer reclaimable on liveness at all. That is exactly #7364's failure
   path, and it made the claim this change is built on ("a failed removal
   costs nothing, the next cycle retries") false.

   RemoveTaskTempDir removes contents first and stops at the first one it
   cannot remove, leaving the marker behind; the lock file goes last, once
   everything else is gone. Both the task-completion path and the GC use
   it. A cleanup that fails now leaves a directory that is still
   lock-bearing and still unlocked, so the next cycle takes the lock and
   retries — for as many cycles as the holder needs.

2. The legacy TTL could delete a live task's TMPDIR.

   Pre-lock directories were reclaimed on age after 72h by default, but
   age cannot establish liveness: DefaultAgentTimeout is 0, so a task may
   legitimately run far longer than any TTL; a daemon on another profile
   can still be running the old binary during an upgrade; and every daemon
   on the machine shares one temp base. The default is now 0 — leave them
   alone — and reclaiming them is an operator's explicit opt-in. Each
   cycle logs how many were left in place so the choice stays visible
   rather than silent.

   Classification also fails closed: only os.ErrNotExist means "no lock
   file". Any other stat error (another user's dir under a shared /tmp)
   now reads as in-use instead of legacy.

Regressions for both: a Windows test that makes a real sharing violation
with CreateFile minus FILE_SHARE_DELETE, asserting the marker survives the
failed cleanup and the directory goes once the handle closes (wired into
the Windows CI job, which runs targeted filters rather than whole
packages); and unix equivalents driving the same paths through an
unlinkable payload, plus fail-closed coverage for an unreadable directory.

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

* fix(daemon): publish the temp dir marker atomically, restore it on rmdir failure

Two more windows in the marker's lifecycle, from review.

1. A directory could be swept between marker creation and lock acquisition.

   LockTaskTempDir created .task_lock and locked it as two steps, so there
   was an instant where the marker existed unlocked. A sweep landing there
   reads exactly what a dead owner leaves behind: it takes the lock,
   concludes the owner is gone, and deletes the directory — after which
   the starting task's own lock call succeeds and it runs with a TMPDIR
   that no longer exists.

   The marker is now locked under .task_lock.claiming and renamed into
   place once held. Rename within one directory is atomic and the lock
   rides on the open file description, not the name, so the marker becomes
   visible and held in the same step.

   Before that rename the directory carries no marker, which classifies as
   legacy — and legacy only reclaims what is OLDER than legacyTTL. A
   directory created microseconds ago is younger than any positive TTL,
   and a TTL of 0 disables the branch outright, so no configuration can
   sweep the pre-publish state either.

2. A failed rmdir left a directory with no marker.

   The marker has to be deleted before the directory can be removed, so a
   failure at that last step — a Windows process holding a handle to the
   directory, or content an orphaned child recreated — left a directory
   nothing could classify again. With the legacy branch disabled by
   default that is a permanent leak, and it was the one path where cleanup
   got furthest. The marker is now put back when that removal fails, so
   every failure mode leaves the retry available.

Regressions: the publication order and the mid-claim on-disk state (spared
under a disabled TTL and under positive ones), a sweeper running flat out
against 200 publish/use/cleanup cycles, and the rmdir-failure branch —
which fails without the restore, verified by removing it.

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

* fix(daemon): decide the legacy sweep on content, not on age alone

The publication window was still open. The claim-then-rename ordering
stopped .task_lock from ever existing unlocked, but a directory mid-claim
carries no .task_lock at all, so the sweep still reaches it through the
legacy branch — and the argument that protected it there was wrong.

That argument was "a directory created microseconds ago is younger than
any positive TTL". MULTICA_GC_TASK_TEMP_LEGACY_TTL takes any duration,
down to a nanosecond, and an owner stalled between MkdirTemp and the
rename outlasts a large TTL just as easily. Reproduced at 1ns, 1ms, 1h
and 72h alike: the sweep deleted a directory whose claim was held.

Age cannot separate a pre-lock leftover from a directory being published,
so stop asking it to. Content can: a directory being published holds
nothing but its own lock files, because the task has not been handed the
path yet and cannot have written anything, while a leftover worth
reclaiming is by definition one with something in it. The legacy branch
now reclaims only directories that hold task content, which covers both
halves of the window — the empty directory before the claim exists, and
the claim-only directory after — at every TTL value, with no timing
assumption left in the argument.

The cost is that a pre-lock daemon which crashed before its task wrote
anything leaves an empty directory nothing reclaims. That is a few bytes
against deleting the temp dir of a task that is starting.

Tests: the mid-claim cases now run at 1ns/1ms/1h/72h with now pushed PAST
the TTL, so they fail if anything reintroduces a timing argument; the
pre-claim (empty directory) half is covered the same way; and the
invariant itself is pinned directly, so relaxing it breaks a test rather
than a race. The existing legacy TTL test now models a real leftover by
giving it content. Verified all of these fail with the content rule
removed.

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

* fix(daemon): re-check the lock immediately before removing a legacy dir

The legacy branch's decision is several separate observations — classify,
then walk for content, then walk for mtime — and a directory can finish
publishing in the middle of them. Classified as legacy while its marker
was still unpublished, then published and written to by its task, and the
removal went ahead without ever looking at the marker that had appeared:
the sweep deletes a live task's TMPDIR. What was accidentally holding this
together was that the task's own write refreshes the mtime — a timing
crutch, in the branch whose whole point is not to rely on one.

Re-read the classification immediately before removing, and skip unless it
is still legacy.

That closes the window rather than narrowing it, because of what content
means: a task only writes into the directory after it has been handed the
path, which happens only after LockTaskTempDir returns and therefore only
after the marker is published and held. So content observed at the top of
the branch, plus no marker seen at the bottom, proves no live task owns
this directory — and none can arrive later either, since MkdirTemp only
ever hands out a fresh empty directory under a new name.

The regression drives that exact interleaving through a barrier: the sweep
commits to removing the directory, the owner then publishes and its task
writes, and the sweep must notice. `now` is far past the TTL so age cannot
mask it. Verified it fails without the re-check.

Also from review: drop a stale test comment that still claimed a positive
TTL can only be configured far above a directory's age, and record in the
config and operator docs that a directory holding no task content is never
reclaimed on age — including a shell left by a CURRENT daemon that died
between creating the directory and publishing its lock, not just an old
daemon's empty leftover.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-21 17:38:44 +08:00
Bohan Jiangandmultica-agent d2532f766b MUL-6491 fix(daemon): fence env-root resets on a live execution, not a task id (#7356)
* fix(daemon): fence env-root resets on a live execution, not a task id

Follow-up to #7347. The identity marker answers WHO owns an env root, not
whether that owner is still running, so Prepare read "owner == my task id"
as "my own rerun, safe to reset".

That is reachable rather than theoretical. ReclaimStaleDispatchedTaskForRuntime
re-delivers a task row whose prepare lease expired using the SAME task id,
and a failing lease renewal only logs — it does not cancel the Prepare still
in flight. The re-delivered execution then wiped the running one's workdir,
config and worktree: the cross-execution deletion the env-root claim exists
to prevent, arriving through the one door it left open.

Add a second signal alongside the marker: an OS advisory lock on the env
root, held for the life of the Environment and released by Cleanup. The
kernel drops it when the holder exits for any reason, so it answers "is the
previous execution still alive?" across processes with no heartbeat, no PID
table and no stale-state cleanup path — a crashed execution's directory is
recoverable on the next dispatch, a running one's is not.

Taking the lock first also serialises the claim, which replaces the
Mkdir/O_EXCL arbitration with something simpler and stronger: no other
execution can be mid-claim, so the checks below it cannot race.

That in turn fixes the torn-marker case from the same review. The marker was
created empty and written a moment later, so a crash in between left a
zero-length file that read back as "no owner" while still counting as
directory content — the env root could then be neither adopted nor reset,
and stayed wedged until someone deleted it by hand. Under the lock an
unnamed marker is simply rewritten.

Tests, each verified to fail without its fix: an overlapping execution of one
task id is refused and the live work survives; the same id recovers once the
prior execution released the root; a torn marker is repaired instead of
wedging.

Refs #7326

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

* fix(daemon): release the env-root lock at the end of the task run

Windows CI caught that the lock outlived the work it guards. It was released
only by Environment.Cleanup, which production never calls — the GC reclaims
env roots on its own schedule — so the lock was effectively held until the
daemon exited. Two consequences: every later dispatch of that task would
fail closed as "held by a running execution", and on Windows the open handle
pinned the directory so nothing could remove it:

    TempDir RemoveAll cleanup: unlinkat ...\.task_lock:
    The process cannot access the file because it is being used by another process

Release it where the execution actually ends. The daemon now defers
ReleaseLock alongside unmarkActiveEnvRoot, which is the same lifetime: the
task run. Cleanup still releases too, and both are idempotent.

Also open the lock file with FILE_SHARE_DELETE on Windows. os.OpenFile does
not request it, and without it Windows refuses to delete a file anyone holds
open — the lock is meant to coordinate executions, not to pin a directory
against the GC. Unix already allows unlinking an open file; this makes
Windows behave the same.

Refs #7326

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

* fix(daemon): hold the env-root claim in the parent, not the prepare helper

Review found the lock never applied in production. Real daemons always
prepare through PrepareIsolated, which runs a short-lived helper process:
the lock was taken inside that helper, the kernel released it the moment the
helper exited, and Environment.lockFile is an *os.File that cannot cross the
helper's JSON response. The parent got back a nil lock, so ReleaseLock was a
no-op and the agent ran with no protection at all — the exact re-dispatch
window this was meant to close.

Every test added with the lock went through the in-process Prepare, where
the "helper" never exits, so none of them could see it.

Move the claim to the daemon parent, whose lifetime IS the task run:

- ClaimEnvRoot / EnvRootClaim.Release are now exported, and runTask takes
  the claim alongside markActiveEnvRoot and releases it on every exit.
- PrepareParams.EnvRootPreclaimed tells preparation the caller already holds
  the claim and performed its reset, so it does not try to take a lock the
  parent is holding. Direct in-process callers still claim for themselves.

Also cover the reuse path, which took no claim at all: a task continuing in
a PRIOR task's env root now excludes on that root through
LockEnvRootForReuse. Identity there belongs to the earlier task, so this is
the exclusion half of a claim only — without it two continuations of one
task could refresh and run the same workdir concurrently. Guarded to roots
under the workspaces root, since a local_directory task's PriorWorkDir is
the user's own directory.

Tests now exercise the production path: one drives PrepareIsolated through
the real helper harness and asserts the parent still holds the claim after
the helper exits, the other covers reuse exclusion. Against the previous
arrangement the first fails with "a second execution claimed the env root of
a task that just prepared".

Refs #7326

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

* fix(daemon): lock a prior env root only after proving it is a managed one

Review found the reuse guard wrote its lock file before it had established
where it was writing. runTask locked filepath.Dir(task.PriorWorkDir) up
front, gated only by a lexical filepath.Rel containment check — which does
not resolve symlinks. A link inside the workspaces root pointing anywhere
else passed that gate, and .task_lock was created in the target: a user's
own directory, or a path outside the root entirely. Eligibility was not
settled until much later, in shouldReusePriorWorkdir.

Reorder so the proof comes first. shouldReusePriorWorkdir already resolves
symlinks and verifies the {workspace}/{task}/workdir shape, managed
provenance and the task-context marker; it now returns the canonical path it
validated, and that is the only path ever opened. The lock is re-confirmed
against the same check while held, closing the gap between proving
eligibility and acting on it.

Declining is not fatal on this path: a busy root, a vanished directory or a
failed re-check falls back to a fresh Prepare, which costs session
continuity rather than correctness. ErrEnvRootBusy makes that distinguish-
able from a real I/O failure. The lexical isUnderWorkspacesRoot helper is
gone — it promised containment it could not deliver.

The canonical path deliberately stays internal. Reuse still receives
task.PriorWorkDir, so the workdir reported to the server and handed to the
next task keeps its existing shape.

Tests: negative cases for a symlink escaping the root, a plain outside path
and a managed-looking path without provenance, each asserting no .task_lock
appears outside — against the previous ordering the symlink case fails with
"prior-workdir guard wrote the lock outside workspaces root". A positive
case pins that a provenanced root IS locked, in the right directory, and
excludes a concurrent continuation. And a helper-process test pins that a
caller who holds the claim but forgets EnvRootPreclaimed fails immediately
and says so, instead of preparing while believing it is protected.

Refs #7326

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

* fix(daemon): claim a prior env root by identity, not by name

Review found the remaining TOCTOU: the canonical path validation returns is
a string, not a handle. Between proving eligibility and opening the lock,
the validated env root can be renamed away and a symlink to somewhere
outside dropped in its place — an ordinary open follows it and creates
.task_lock out there, and no later re-check can undo a write that already
happened. A swap to a different but equally provenanced directory at the
same path is worse still: every name-based check agrees, and the daemon ends
up holding a lock on one directory while running in another.

Resolve the lock through an os.Root pinned at the workspaces root. Root
refuses any component that resolves outside its tree, so the lock file
cannot land outside no matter when the swap arrives — containment stops
being a check that races and becomes a property of the open.

Root still follows symlinks that stay inside the tree, so containment alone
cannot answer "is this the same directory?". Chain identity instead: stat
the directory validation approved, require the lock to have landed on that
same inode, and require the directory handed to Reuse to be it as well.
os.SameFile compares identity; comparing canonical path strings cannot tell
a directory apart from its replacement at the same name.

Reuse now receives the canonical path that was locked, so the object locked
and the object used are the same one. Two consequences that fell out:

- The workspaces root has to be resolved before measuring the env root
  against it, or a symlinked root (macOS /tmp, a home on a linked volume)
  makes them look unrelated and every reuse is refused.
- gateResumeToReusedWorkdir compared workdirs textually, so a canonical
  env.WorkDir silently dropped the prior session. It now compares directory
  identity, which is what it meant all along.

Tests use a deterministic hook to swap the directory between validation and
the lock rather than racing it. Against the previous code they fail with
"validated canonical root was retargeted and the lock was written outside"
and "accepted reuse after the validated directory was replaced at the same
path".

Refs #7326

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

* fix(daemon): pin the workspaces root before validating, and lock from one handle

Three findings from review, all in how os.Root was being used rather than in
the choice of it.

Pin the workspaces root BEFORE validating anything. Opening it afterwards,
from a name validation had just approved, re-resolved that name: rename the
whole root aside, leave a symlink to a look-alike tree, and os.Root
faithfully pins the replacement. Root promises you cannot escape the tree it
opened — not that it opened the tree you meant. Holding the handle first
makes that ordering impossible.

Take the directory's identity and create its lock file from ONE sub-Root
pinned on the env root. Resolving the env root's relative path twice from
the workspaces Root — once to stat, once to create — allowed A on the first
resolution and B on the second, reporting A's identity while holding B's
lock.

Close the last window by detection rather than by claiming it is closed. A
file descriptor cannot cross into the preparation helper process, so Reuse
can only be handed a name, and the gap between the final identity check and
Reuse's own opens cannot be pinned away. The environment Reuse produces is
now checked back against the directory the lock is held on, so a
substitution ends the run in a fresh environment instead of executing in a
directory nothing holds. What remains uncovered is stated in the code: the
writes Reuse itself performs inside that window.

Also drop sameExistingDir's string fast path, which returned true without a
stat and contradicted the identity contract the function documents. Its
table test used synthetic paths that never existed; they are real
directories now, which is what the comparison was always about.

Tests, each verified to fail without its fix: a workspaces-root swap must
not let the lock be written into the replacement tree; reported identity
must be the directory the lock file is actually in; and a swap after the
claim, before Reuse, must end in a fresh environment rather than the
substitute.

Refs #7326

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

---------

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

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

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-21 13:41:21 +08:00
Bohan Jiangandmultica-agent 3622db21cf MUL-6491 fix(daemon): give each task an exclusively claimed env root (#7347)
* fix(daemon): derive task env roots and branches from the full task id

Task ids are UUIDv7, whose leading 8 hex chars are the high 32 bits of a
48-bit millisecond timestamp: they only advance once every 2^16 ms
(~65.5s). Deriving the env-root segment from that prefix meant every task
created inside one such window landed on the same directory.

Prepare removes an existing env root before recreating it, guarded only by
the comment "task IDs are unique". That premise held for the id but not for
the truncation, so the second task's Prepare ran os.RemoveAll over a live
sibling's env root — taking its workdir (including a worktree with
uncommitted work), output/, logs/ and the 0700 multica-config/ with it.
Overwritten CLAUDE.md / AGENTS.md identity files were the visible symptom;
the deletion was the actual damage.

Use the whole id for the env root and for the agent/<name>/<task> branch
names, which truncated the same way and collided for concurrent tasks of
one agent. shortID stays for logs and display only. Prepare now derives its
root through PredictRootDir so the two can no longer drift apart.

Existing short-prefix directories need no migration: the GC enumerates task
dirs with ReadDir and reads .gc_meta.json, so it still reclaims them.

Fixes #7326

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

* fix(daemon): take the task path segment from the id's random tail

The first attempt used the whole 32-char task id, which overflowed Windows
MAX_PATH: a branch name becomes a path under .git/refs/heads/ inside an
already-deep task checkout, and CI failed with

    cannot lock ref 'refs/heads/agent/windows-codex/<32 chars>':
    Filename too long

Length was never the fix. The bug was which end the segment came from: a
UUIDv7 leads with a 48-bit millisecond timestamp, so its first 8 hex chars
are the high 32 bits and only advance once every ~65.5s. Read from the tail
instead, where the id is random, and 12 chars are enough — 4 more than the
original segment rather than 24.

That trades a certainty for a probability, so make the destructive step
prove itself instead of assuming: Prepare stamps a .task_owner marker into
the env root and refuses to run os.RemoveAll over a root another task owns.
A rerun of the same task still resets its own directory. A task that fails
to start is recoverable; one that deletes a running sibling's uncommitted
work is not.

Two existing fixtures used synthetic ids differing only in the head and so
shared a tail. The guard caught them; they now differ where a real UUIDv7
does.

Fixes #7326

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

* fix(daemon): make the env-root claim atomic instead of check-then-delete

Review caught that the ownership guard did not cover the case it was added
for. It read .task_owner, then deleted, then wrote the marker — three steps,
so two tasks sharing a taskKey could both observe "no owner", and the slower
one would still os.RemoveAll the faster one's live directory. The guard only
held when a foreign marker was already durably in place, i.e. serially.

Replace it with a claim protocol built on two atomic filesystem primitives:
os.Mkdir fails if the directory exists, and O_EXCL fails if the marker
exists. Exactly one racing caller wins each, and a caller that wins neither
never reaches the destructive path.

The claim is then held for the rest of Prepare. A rerun clears the env
root's CONTENTS and leaves the marker in place rather than removing and
recreating the directory, so there is no window in which the root looks
unowned to a racing task.

A directory that holds files but names no owner now fails closed instead of
being deleted as abandoned. The marker is written before any content, so the
only unowned directory that is safe to adopt is an empty one — the residue
of a crash between Mkdir and the marker write, which would otherwise wedge
the task permanently.

Tests: a barrier-synchronised concurrent Prepare asserting exactly one task
starts and the winner's tree survives (fails against the previous sequence
with "directory not empty" / "invalid argument" — the deletion racing the
create), plus fail-closed and empty-adoption cases. repocache carried a
third copy of the task-segment rule with no test; it now has the same
contract test as the handler.

Fixes #7326

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

---------

Co-authored-by: multica-agent <[email protected]>
2026-08-21 00:36:59 +08:00
e1be1625cb MUL-5959: feat(agent): add Dim (DimCode) ACP runtime (#6675)
* feat(agent): add Dim (DimCode) ACP runtime

Add Dim (dimcode, the `dim` CLI) as a first-party agent runtime, driven
over the ACP (Agent Client Protocol) transport via `dim acp`.

Key integration points:
- New dim backend (pkg/agent/dim.go): spawns `dim acp`, performs the
  ACP initialize/session/new handshake, and raises the runtime's
  hardcoded read-only permission preset to full-access (plus agent mode)
  via session/set_config_option before the first prompt — without it every
  file write is denied by a capability rule. Model override uses
  session/set_model; the model catalog is read from session/new.
- Session resume is intentionally skipped: Dim binds sessions to the
  creating process, so a later process's session/load is rejected with
  "held by another process" even after a clean session/close. The backend
  always starts a fresh session and reports ResumeRejected so the daemon
  classifies the run correctly. A best-effort session/close keeps Dim's
  own session list free of orphaned entries.
- Registration: SupportedTypes whitelist, New() factory, launch header,
  daemon probe (MULTICA_DIM_PATH / MULTICA_DIM_MODEL), default agent
  command name, protocol_family CHECK migration (255), model discovery,
  MCP config support, metrics label, and runtime display.
- Docs/UI: provider logo, landing i18n (en/zh/ja/ko), providers and
  install-agent-runtime docs, README, CLI_AND_DAEMON, SELF_HOSTING.
- Tests: dim backend unit tests (fresh session + resume behavior), shared
  ACP deliverable case, logo and MCP-support tests.

* feat(views): use official DimCode app icon for Dim provider logo

Replace the placeholder inline SVG with the official DimCode desktop
client app icon (from /opt/DimAgent/resources/build/icon.png, resized
to 128px), imported as a static asset the same way the Qwen Code mark
is handled.

* test(agent): add real dim ACP smoke tests (agentintegration)

Two gated tests behind MULTICA_RUN_REAL_AGENT_SMOKE=1:

- TestDimRealACPSmoke: full end-to-end against real `dim acp` —
  initialize, session/new, set_config_option (permission/mode), prompt,
  and assert completed output.
- TestDimRealResumeRejected: pass a fake ResumeSessionID, assert the
  backend starts a fresh session (no session/load), and reports
  ResumeRejected=true so the daemon classifies the run correctly.

Verified against dimcode 0.3.2 and 0.3.8.

* fix: renumber dim migration 265 → 271 (upstream added 265-270)

* fix: renumber dim migration to 272 (upstream added 271)

* fix(agent): dim cross-run resume via session/load + review fixes

Address review feedback on the Dim ACP runtime:

- #1 Deliver runtime brief to Dim: add dim to the AGENTS.md provider
  list (verified dim 0.3.8+ reads AGENTS.md from the session cwd).
- #2 Cross-run session continuity: dim 0.3.10+ releases its per-process
  session lock ~5s after the owning process exits, so resume now goes
  through the standard ACP session/load (same as traecli/kiro/grok)
  instead of always starting fresh. A loaded session retains its
  permission/mode, so set_config_option runs only on fresh sessions.
  Add a two-run regression (TestDimRealCrossRunResume) proving run B
  recalls context established only in run A.
- #3 Process lifecycle races: the deferred cleanup now cancels the run
  context before cmd.Wait (a child ignoring stdin EOF no longer hangs
  Result delivery) with a bounded force-kill fallback; the success path
  waits for the final prompt notification with a grace window instead
  of a non-blocking read that could miss the last message.
- #4 Isolate model discovery by executable path (discoveryCacheKey).
- #6 Real smoke test now writes a sentinel file to prove full-access is
  effective; a best-effort session/close is sent before teardown.
- #7 gofmt.

* test(agent): add dim process-lifecycle regressions (review #3)

- TestDimCleanupKillsHangingChild: a child that ignores stdin EOF and
  SIGTERM after an early failure is force-killed within
  dimProcessWaitTimeout, so Result still closes.
- TestDimPromptMissingNotificationStillCompletes: when session/prompt
  returns without stopReason (onPromptDone never fires), the bounded
  final-notification wait falls through and Result completes instead of
  hanging.

* fix(migrations): renumber dim runtime_profile migration 272 → 273 (upstream added 272)

* fix(agent): review round 2 — process tree, quiescence, fail-closed, version check

Address all remaining blockers from the second review:

- #1 Require dim >= 0.3.10 (version check at initialize); bounded retry
  on session/load when the lock is not yet released instead of silently
  starting fresh; integration test no longer sleeps before resume.
- #2 Process-tree cleanup: configureProcessGroup + signalProcessGroup +
  waitProcessGroupGone; cmd.Cancel returns nil so we own all signalling;
  regression asserts the descendant PID is gone.
- #3 Notification quiescence: onActivity + waitForACPNotificationQuiescence
  (same as hermes); Dim equivalent of the late-final-notification test.
- #4 Permission fail-closed: set_config_option runs on BOTH fresh and
  resumed sessions; on config failure, session/close is sent before
  returning so a partially configured session is not resumed; regression.
- #5 Migration renumbered 273 → 274 (upstream added 273).
- #6 AGENTS.md mapping regression, two-executable cache isolation test,
  session/close unit assertion, smoke test now executes a command.
- #7 README.md + CLI_AND_DAEMON.md updated with 22 CLIs including Dim.

* chore: remove temporary review actions doc (not for PR)

* fix(migrations): renumber dim migration 274 → 310 (upstream added 274-309)

* review: fix retry break scope, dedup isACPSessionNotFound comment, update stale notes

* fix(agent): pass *exec.Cmd to signalProcessGroup/waitProcessGroupGone (upstream signature change)

* fix(agent): close session on set_model failure + resume regression test

Audit-found gaps from self-review:
- set_model failure now sends session/close before returning (same as
  set_config_option failure) — reviewer #4 said 'permission, mode, or
  model'.
- Add TestDimConfigFailThenResumeReestablishes: run A config fails →
  session/close sent → run B resumes and re-applies set_config_option
  (fail-closed), completing successfully.
- Fix two stale comments that contradicted the code (config block now
  runs on both fresh and resumed sessions).

* fix(agent): add WaitDelay, use labeled break in retry loop

Self-audit improvements:
- Add cmd.WaitDelay=10s for consistency with claude.go (hard backstop
  if a process somehow survives SIGKILL).
- Use labeled break (break loadRetry) so runCtx cancellation during the
  retry delay exits the for loop directly, not just the select.
- Update PR description: migration 273→310, set_config_option now
  re-applied on both fresh and resumed sessions.

* fix(migrations): renumber dim migration 310 → 313 (upstream added 310-312)

* fix(migrations): remove stale 313 dim migration (replaced by 314 after upstream added dsh at 313)

* fix: DSH compatibility + thinking levels + reviewer round 3

Systematic fix of all 39 items from the compatibility gap analysis:

Rebase + migration:
- Rebase to latest upstream/main (resolves all DSH conflicts)
- Migration 314_runtime_profile_add_dim (whitelist includes both dsh + dim)
- Remove stale 313 dim migration

DSH + Dim coexistence (provider lists, counts, docs):
- SupportedTypes, config.go, metrics labels: both dsh + dim
- i18n (4 files): count 23, lists include DSH + Dim + Oh-My-Pi
- README.md/README.zh.md: count 23
- CLI_AND_DAEMON.md: 23-row table
- environment-variables.mdx (4 langs): MULTICA_DIM_PATH/MODEL callout
- display.ts, mcp-support.ts, types/agent.ts, provider-logo.tsx: both dsh+dim

Thinking levels (was MISSING — dim supports thought_level):
- dim.go: call applyACPEffortOption after set_model
- thinking.go: add dim to acpCatalogThinkingProviders
- dim.go: retain sessionResult for effort option

Reviewer round 3:
- Version check fail-closed: empty/malformed → reject (not allow)
- Cache test through ListModels call site with fake executables
- set_model failure regression test
- Test companions: sidecar_manifest_test, runtime_config_test add dim

Code quality:
- agent.go: fix unreachable duplicate return (rebase artifact)
- dim.go: labeled break in retry loop, WaitDelay, session/close on set_model fail

* fix: deliverable test fake version + provider-logo rebase artifact

* fix(migrations): renumber dim migration 314 → 315 (upstream added 314_workspace_mcp_config)

* fix(migrations): renumber dim migration 315 → 319 (upstream added 315-318)

* fix: Windows process tree, MinVersions, retry tests, migration 327

Address all 5 remaining blockers from review round 4:

1. Windows process-tree ownership: use startOwnedProcessTree instead of
   cmd.Start(), releaseProcessGroup in cleanup defer. This attaches the
   child to a Job Object on Windows (no-op on Unix), ensuring descendants
   are captured and terminated.

2. Migration renumbered 319 → 327 (upstream added 319-326).

3. Add dim: 0.3.10 to MinVersions in version.go so the daemon registers
   old Dim binaries as offline and refuses triggers (defense in depth on
   top of the ACP agentInfo.version check in dim.go).

4. Session-lock retry contract tests: success after bounded retries,
   exhaustion without falling through to session/new. Fake script
   supports DIM_LOAD_HELD_N (held for N calls then succeed) and
   DIM_LOAD_HELD_ALWAYS (always held).

5. README.md:206 20→23 runtimes. PR description updated.

* test: add missing reviewer-requested regressions (round 4)

- Windows Job Object regression: dim_windows_test.go proves the Dim
  backend captures and terminates descendants via startOwnedProcessTree
  (Windows-only build tag; companion to TestStartOwnedProcessTree).
- MinVersions registration: add dim test cases to TestCheckMinVersion
  (0.3.10 ok, 0.3.9 rejected, invalid rejected).
- Retry cancellation: TestDimSessionLoadRetryCancelled cancels the
  context during the retry delay, asserts no session/new fallback.

* fix: sync upstream Command signature changes + restore dim additions

Upstream changed ListModels/discoverXxxModels/detectCLIVersion to accept
Command (struct with Path+Prefix) instead of string. The previous rebase
kept our old-signature versions, causing widespread compile failures.

Restored all affected files from upstream, then re-applied dim-specific
additions:
- agent.go: dim in SupportedTypes, New(), launchHeaders
- hermes.go: isACPHeldByProcess + -32002 in isACPSessionNotFound
- thinking.go: dim in acpCatalogThinkingProviders
- models.go: dim case in ListModels + discoverDimModels
- version.go: dim in MinVersions
- All dim test files preserved (dim_test.go, dim_integration_test.go,
  dim_windows_test.go, models_test.go dim cache test)

* fix: rebase to latest main, migration 327→341, resolve DetectVersion conflict

* fix: remove unused os/exec import in dim_windows_test.go

* fix(migrations): renumber dim migration 341 → 342 (upstream added 341)

* fix: use Command.exec instead of exec.CommandContext (upstream GH #7046)

Upstream's TestOnlyLaunchGoSpawnsRuntimeProcesses requires all backends
to build processes through Command.exec (launch.go), so a custom runtime's
fixed_args are carried into the subprocess. Dim was the only backend still
using exec.CommandContext directly.

* fix: launch prefix policy + remove empty test files + deterministic retry tests

Address all 4 findings from review round 5:

1. Add "dim": dimBlockedArgs to launchPrefixBlockedArgs so protocol-breaking
   flags (--help/--auth-setup/--remote) are filtered from fixed_args.
2. Remove 77 zero-byte *_test.go files accidentally added at repo root.
3. Make retry tests deterministic: TestDimSessionLoadRetryCancelled waits
   for the first session/load request before cancelling (no fixed sleep);
   TestDimSessionLoadRetryExhausted asserts exactly 4 load attempts.
4. PR description will be updated separately.

* fix: rebase to latest main, migration 342→343 (upstream added mcode at 342)

* chore: trigger CI

* chore: refresh PR

* fix: restore mcode that was lost during merge conflict resolution

Merge took 'ours' side which predated mcode. Restore mcode in:
- SupportedTypes, New() factory, launchHeaders
- agent_supported_types_test.go want map
- metrics/labels.go
- config.go defaultAgentCommandNames
- migration 343 up/down whitelists

* fix: restore mcode probe in agents_probe.go + agent-cli-command-names.txt

* fix: restore all remaining mcode references lost during merge

Files fixed:
- README.md: mcode row in runtimes table
- mcp-support.test.ts: mcode assertion
- config.go: mcode in Agents comment + error message
- runtime_config.go: mcode in AGENTS.md case
- version.go: mcode in MinVersions
- version_test.go: mcode test cases

Verified: no file has fewer mcode references than upstream.

* fix: restore mcode in environment-variables docs + CLI_AND_DAEMON ACP list

- environment-variables.{mdx,ja,ko,zh}: restored 'MiniMax Code' in the
  QwenPaw model-variable sentence
- CLI_AND_DAEMON.md: added MiniMax Code to ACP-family list + loadSession
  fallback description

Verified: every changed file now has >= mcode references vs upstream.

* fix(migrations): renumber dim migration 343 → 344 (upstream added 343)

* fix(migrations): renumber dim migration 344 → 348 (upstream added 344-347 plugin migrations)

* fix(migrations): update down migration comment 344 → 348

* chore: retrigger CI (flaky TestOpenclawDiscoveryCacheConcurrentPreparations — upstream test, passes locally x5)

* fix(migrations): renumber dim migration 348 → 352 (upstream added 348-351)

* fix: Dim launch-prefix regression test + version.go comment repair (review #6)

- Add TestDimLaunchPrefixFiltersBlockedFlags: proves allowed prefix
  reaches command before acp, and --help/--auth-setup/--remote/-h are
  stripped from Dim's launch prefix.
- Add dim to TestLaunchPrefixReachesACPFamilies family list.
- Repair version.go: dim's cross-run session/load comment was appended
  to mcode entry during a rebase conflict; restore two independent
  accurate comments.

* fix(migrations): update down migration comment 348 → 352

* chore: retrigger CI (backend-tests stuck 50+ min)

* fix(migrations): renumber dim migration 352 → 362 (upstream added 352-361)

* fix: use logAgentCommand for dim argv logging (upstream redaction requirement)

Upstream's TestOnlyLaunchGoLogsAgentCommandArgs enforces that all
runtime argv logging goes through Config.logAgentCommand so sensitive
args are redacted. dim.go was still using Logger.Info directly.

* fix(migrations): renumber dim migration to 370

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

Review fixes from #7245 (Elon):

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

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

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

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

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

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-20 11:32:17 +08:00
de902a4aab MUL-6273: feat(agent): add MiniMax Code ACP runtime (#7035)
* feat(agent): add MiniMax Code ACP runtime

* fix(agent): terminate mcode process trees

* fix(agent): address mcode review blockers

* test(agent): keep mcode model contract API-stable

* fix(migrations): resolve mcode prefix collision

* fix(agent): launch mcode through runtime command

Assisted-by: MiniMax Code

* fix(migrations): renumber mcode migration

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

---------

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

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

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

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

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

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

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

---------

Co-authored-by: Sol-Boy <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-14 13:57:09 +08:00
a1da8e536f MUL-6000 fix(daemon): link user Codex skills into codex-home instead of copying them (#6742)
* fix(daemon): link user Codex skills into codex-home instead of copying them

Every Codex task got a full recursive copy of ~/.codex/skills/ inside its
per-task CODEX_HOME. For a user with npm-backed skills that is ~100 MB per
task directory, re-copied on every task start — hydrateCodexSkills wipes and
re-seeds on both the Prepare and the Reuse path — and never reclaimed: the GC
artifact sweep only matches node_modules / .next / .turbo, and whole-directory
reclaim needs the issue to reach a terminal status first. Total cost scales
with the number of live task directories.

Link instead. The same per-task home already links its other two shared
resources, sessions and the plugin cache, and a skill directory is read-only
input to the CLI, so the write isolation a copy incidentally bought has no
requester. EvalSymlinks still runs first, so an installer-shipped symlink
(lark-cli into ~/.agents/skills/) resolves to the real directory rather than
producing a link to a link.

The three paths that could reach through the new link are unchanged in
behavior: os.RemoveAll deletes a link and not its target, writeSkillFiles
probes with os.Lstat so a workspace skill colliding with a linked user skill
lands in a `-multica` sibling, and the GC refuses to walk into links. Tests
pin all three, plus a windows-tagged test for the junction form of the link.

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

* fix(daemon): stop GC walks from descending into Windows directory junctions

The GC's artifact sweep and its size accounting both refuse to follow links
out of a task directory, but they tested for os.ModeSymlink alone. That misses
the link shape Windows actually produces: createDirLink falls back to a
directory junction (mklink /J) whenever os.Symlink is denied, which is the
default without Developer Mode, and since Go 1.23 os.Lstat reports a junction
as ModeDir|ModeIrregular with no ModeSymlink bit while its DirEntry still
answers IsDir() == true. filepath.WalkDir therefore descends straight into the
link target, and the sweep would delete a node_modules / .next / .turbo
directory that lives inside the user's real home.

Match both mode bits in all three walks. The task directory links user content
in three places today — the Codex session store, the plugin cache, and now the
user's skills — so this is reachable on any Windows host running Codex tasks.

Windows-tagged tests create the junction with mklink /J directly, so the
junction shape is covered even on a runner where symlinks are permitted, and
they run in the existing windows-execenv CI job.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-11 13:51:50 +08:00
rongchenzheng 586a624aa5 fix(cli): isolate task profile state (#6407) 2026-08-10 12:20:17 +08:00
545d890dae feat(cli): add --compact to issue comment list, adopt it in every agent read command (MUL-5442) (#6546)
* feat(cli): add --compact to issue comment list, adopt it in every agent read command (MUL-5442)

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

Review catches by Elon on #6493, both accepted:

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

---------

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

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

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

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-05 21:15:13 +08:00
2bb13667c6 refactor(daemon): cross-channel command dedup — brief states the loop, per-turn carries the commands (MUL-5442) (#6421)
* refactor(daemon): move the workflow's command templates to the per-turn channel (MUL-5442)

Cross-channel dedup, brief side. MUL-5721 measured that every issue-turn
variant of the per-turn message already carries the ready-to-run commands
with real ids (issue-get line, reading hints, reply cookbook), fresher
than the brief's static copies. The six workflow steps now state the loop
shape — command names and flag mnemonics only; full templates and every
embedded issue UUID leave the brief. The Ownership status commands and the
squad-activity call switch to <issue-id> placeholder form.

Doctrine pins stay on the brief side (mandatory catch-up, scan-first
order, both motivation anchors); command-template pins re-anchor to names
and mnemonics. Two design-consequence test updates: the static-catch-up
assertion repoints at the doctrine (the full command now lives in every
per-turn variant), and the byte-identity non-vacuity guard varies agent
identity instead of issue id — because the brief is now deliberately
issue-id-independent, which opens the cross-issue prefix-cache door.

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

* refactor(daemon): point the reply cookbook at Comment Formatting instead of restating it (MUL-5442)

Cross-channel dedup, per-turn side. The cookbook's prose restated the
brief's Comment Formatting rules (file-first rationale, inline-content and
HEREDOC hazards, the full Windows $OutputEncoding mechanics) around the
command that already demonstrates the shape. It now keeps the file-first
order, the ready-to-run command, the literal-newline rule, and the
pointer; the hazard mechanics live once, in the brief section the pointer
names. Default cookbook: 639 -> 494 bytes (-145 per reply turn,
uncacheable channel); the Windows variant sheds ~200 more.

Pins re-anchor from the deleted prose to the surviving anchors (file-first
order, the pointer, the command form); the MUL-2904/#4182 banned-shape
negative guards are untouched.

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

* fix(daemon): narrow the per-turn coverage claim, test the cross-issue invariant, strip the command shape from the guest-visible prohibition (MUL-5442)

Review catches by Elon on #6421 plus the CI failure, all three related:

1. The steps header overstated what the per-turn message carries — it
   ships the issue id and ready-to-run context-read commands; other calls
   are assembled from Available Commands. Reworded to say exactly that (a
   factual claim in a prompt cannot rely on the reader discovering it is
   wrong).
2. The cross-issue byte-equality this PR claims as a design benefit is now
   asserted directly per provider (same stable inputs, different 36-char
   issue UUIDs, byte-identical brief) — a Contains-based negative cannot
   catch truncated/transformed/conditional id use.
3. CI: the squad guest-leader contract test bans any runnable
   'issue status <issue-id> in_review' shape in guest-visible text; the
   placeholder rewrite made the leader dispatch rule's NEGATIVE sentence
   match that ban. The prohibition now states itself without a command
   form ('do NOT move it to in_review or done on this turn') — negative
   sentences should not carry copy-pasteable command shapes at all.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-05 14:29:01 +08:00
b98f760f1f MUL-5604: 支持 Reasonix runtime (#6370)
* feat(agent): add Reasonix runtime

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

* fix(agent): make Reasonix sandbox host-adaptive

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

---------

Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-05 14:19:28 +08:00
c3aab05a38 refactor(daemon): rewrite Background Task Safety to its judgment form (MUL-5442) (#6381)
* refactor(daemon): rewrite Background Task Safety to its judgment form (MUL-5442)

Owner-authorized judgment rewrite per the Claude 5 context-engineering
guidance: state the platform fact and the boundaries an agent cannot
infer, drop the enforcement details a frontier model derives.

The section is now three paragraphs: (1) the fact everything derives from
— turn exit is task-terminal, no wakeup exists, never background-and-yield
— with the foreground-collect, synchronous-fallback and no-standing-by
consequences stated once; (2) the external-systems/CI boundary: not
run-owned, the named --watch ban (kept because MUL-5223 proved the
principle alone did not stop CI-watching), merge-gate is not acceptance
criteria, the hand-off phrasing, and the single explicit-ask exception;
(3) the persistent-service handoff contract, review-locked verbatim.

Deliberately dropped as derivable: the run-owned work enumeration, the
tool-promise enumeration, the wait/collect split rule, the
persistent-service scope bullet, auto-merge and snapshot elaborations.

Both pin lists rewritten to the surviving semantic anchors (21 each);
retired pins are listed in the test comments with the rationale.

2,980 -> 1,931 (-1,049). Incident history stays in the Go comment where
it costs no context.

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

* fix(daemon): delete the duplicated CI exception, pin the full ban clause, sync the comment ledger (MUL-5442)

Review catches by Elon on the judgment rewrite, all three accepted:

1. The old exception bullet survived below the handoff paragraph — the CI
   exception appeared twice, the second copy scope-widened by its
   position, and every substring pin stayed green. Deleted; both BTS
   tests now count-guard exactly one 'The one exception' occurrence.
2. The compound watch/poll ban was pinned only by its first command; the
   full clause is the MUL-5223 boundary, so the pin now covers all three
   members.
3. The Go comment ledger still described the pre-rewrite bullet model
   (retired pins as kept, a forward reference that no longer exists).
   Rewritten to the three-paragraph model so the safety-decision history
   reads true.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-05 13:30:50 +08:00
18f8f78af5 refactor(daemon): per-turn message slimming — OPT-1 UUID dedup + OPT-2 initiator prose (MUL-5721) (#6382)
* refactor(daemon): drop duplicate UUID commands from per-turn comment hints (MUL-5721)

OPT-1 from the MUL-5721 measurement: within one hint, a second full
`multica issue comment list <uuid> ...` command restated the issue UUID
(and, in the resumed hint, both anchors) for no routing value. The warm
hint's issue-wide catch-up and the cold hint's roots scan become flag
swaps on the thread command they follow; the resumed hint loses its
anchor-restating sentence (the read command carries the thread anchor,
the reply cookbook carries the trigger id as --parent).

Fixture-measured: warm hint 555->481, resumed 551->417, cold 429->396;
reply-turn totals -74/-134/-33 B. Ownership prompt and the reply
cookbook (with its concrete --parent command) are untouched.

Every dropped duplicate gets a deny assertion so it cannot silently
return; the demoted fallbacks get semantic anchors in its place.

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

* refactor(daemon): compress Task Initiator attribution prose (MUL-5721)

OPT-2: the block's 400 B attribution paragraph carried two derivable
restatements ("in a workspace many people can reach", "so this
attribution does not widen what you can read or write"). The compressed
paragraph keeps every rule: per-person privacy/access rules apply, the
initiator (not the runtime owner) is who you are answering, credentials
stay scoped to the runtime owner, and the initiator may not see
everything you can. Both MUL-2645 test-pinned phrases survive verbatim;
the fact line and heading are untouched.

Fixture-measured: block 513->370 B (member+email form). The two
previously unpinned surviving rules gain anchors ("is who you are
answering", "do not assume the initiator can see everything you can").

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

* refactor(daemon): restore initiator read/write authorization boundary (MUL-5721)

Review must-fix (#6382): the compression treated "attribution does not
widen what you can read or write" as derivable from credential scoping
and dropped it. It is an independent boundary: credential scoping names
whose credentials run, the visibility sentence constrains disclosure —
neither stops the model from treating an initiator's request as extra
authorization. Restored as a short explicit negation with a semantic
anchor.

Initiator block 370 -> 440 B (still -73 vs the original 513); typical
reply turn 2,191 B (net -147 for the PR).

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-05 13:02:51 +08:00
1e661bb04b refactor(daemon): stage-1 judgment rewrite — mode router and small guardrail sections (MUL-5442) (#6365)
* refactor(daemon): compress the mode router and Reply-block preamble to their facts (MUL-5442)

Stage 1 batch A of the judgment rewrite. The mode-router paragraph spent
~800 bytes explaining WHY the file cannot know the turn mode; the fact an
agent needs is only WHERE the mode is named and that exactly one mode block
applies. Compressed to a single Turn-mode lead carrying every routing fact:
both mode lines, the shared-steps note, the one-block rule, and the
no-mode-line default. The Reply block's first bullet drops its derivations
(session history, confusion warnings) and keeps the id-source rule; the
reply-warranted bullet keeps both pinned anchors and every behavioural
outcome, dropping only the worked examples.

Re-pins: the 'Mode router' heading anchor becomes '**Turn mode.**'.
Co-authored-by: multica-agent <[email protected]>

* refactor(daemon): trim the mechanism prose from the small guardrail sections (MUL-5442)

Stage 1 batch B. Comment Formatting (both variants), Issue Body
Formatting, and Always Use the CLI keep every ban, every incident id, and
every pinned phrase verbatim; what goes is the mechanism narration (what
the shell rewrites, why the CLI rejects outside paths, the resource-type
enumeration). Zero test changes — every pin survives as-is.

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

* fix(daemon): restore two platform boundaries and pin the routing rules (MUL-5442)

Stage-1 review catches by Elon on #6365:

1. Two behavioural boundaries had been deleted as explanation. The
   issue-body H1 rule narrowed 'body or description' to 'body' — but
   'description' is the CLI/API field name, so the alias is a
   cross-surface mapping. And step 2 lost 'CLI failures are normal' —
   platform failure semantics (a failed metadata read must not block the
   task), not tool mechanics. The second deletion also went undisclosed
   in the PR description. Both restored; both now pinned.

2. The compressed Turn-mode lead was guarded only by its markers: every
   routing rule (shared steps, apply-exactly-one, status difference,
   no-mode-line fallback, no status change) could have been dropped with
   green tests. All five now pinned as semantic anchors, and both Reply
   outcomes (work-produced arm, silent-exit arm) pinned individually
   alongside the existing heading anchors.

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

* fix(daemon): pin the full no-mode-line mapping and gofmt the touched tests (MUL-5442)

Final-review catches on #6365: runtime_config_kind_test.go:99 was not
gofmt-clean (a scripted insertion), and the no-mode-line fallback was
pinned as two separable halves — 'No mode line' plus a 'Reply mode' that
other file content satisfies — so the text could mutate to route the
fallback at Ownership mode with green tests. The anchor is now the exact
mapping clause 'No mode line → Reply mode'; the status anchor stays.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-04 18:01:46 +08:00
dc60429366 refactor(daemon): slim Issue Metadata to its judgment core, defer discipline to the skill (MUL-5442) (#6351)
* refactor(daemon): fold the metadata ban list into the write rule (MUL-5442)

The Issue Metadata section spent 1,024 bytes teaching a KV bag. The
What-NOT-to-pin bullet was a separate heading restating the write rule's
negative space; it folds into Write on exit as one sentence with every ban
kept explicitly (secrets/tokens/API keys, logs or comment summaries,
runtime bookkeeping, single-run details). The parenthetical examples and
connective prose go; every rule stays.

Pins updated in kind: the merged ban list is pinned as one sentence plus
the result-comment redirect, so the next compression pass cannot drop a
ban without failing CI. -202 bytes (16,099 -> 15,897 on the standard
fixture).

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

* fix(daemon): restore the runtime-bookkeeping examples that define the ban (MUL-5442)

Review catch by Elon on #6351: 'runtime bookkeeping' has no other
definition anywhere in the brief or skills, so its examples (attempts, run
timestamps, agent IDs) are the category's boundary, not connective prose —
and all three are high-probability miswrites that can look like re-readable
diagnostics or durable facts. Restored inside the merged bullet.

Also adopts the review's pin advice, which is our own anchor-pin doctrine
applied properly: the whole-sentence pin from the previous commit is
replaced with separate semantic anchors (each ban plus the bookkeeping
examples plus the result-comment redirect), so the sentence can be reworded
later without CI churn while dropping any single ban still fails.

+52 bytes; the section lands at 874 (from 1,024), the PR nets -150.

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

* refactor(daemon): defer the metadata write discipline to the working-on-issues skill (MUL-5442)

Owner decision on MUL-5442: metadata is deliberately free-form custom
key-value state — the recommended-keys block never matched the feature's
intent and is removed outright, not relocated. The full ban list defers to
the multica-working-on-issues skill, which already carried a near-complete
copy; the skill gains the two bans it lacked (secrets/tokens/API keys,
agent ids) so nothing loses its home.

The brief keeps only what the interface cannot express: the read-as-hints
stance, the will-a-future-run-re-read-it bar, and the two write-time
boundaries (never secrets, never long content). Section: 874 -> 505 bytes
(1,024 at the round's start).

Pins move with the content: the brief side slims to the surviving
semantics plus the skill pointer; the skill-side contract test gains the
relocated ban anchors so the pointer cannot dangle.

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

* fix(daemon): remove the curated key list from the skill and restore the ban categories (MUL-5442)

Round-3 review catches by Elon on #6351, both accepted:

1. The recommended-keys concept survived in the skill — which loads exactly
   when an agent is about to write metadata, so it still steered free-form
   KV into a platform vocabulary. The owner's ruling was that the concept
   should not exist, not that it should move. The key list, the
   'high-signal keys only' heading, and the pr_url-specific example are
   gone; the section now describes free-form durable custom state and a
   generic set example. A mustNotContain guard keeps the curation from
   creeping back.

2. The relocated ban list had dropped its two defining categories (runtime
   bookkeeping, other single-run details), leaving unlisted values looking
   writable. Restored with the reviewer's structure: each category names
   its examples, and the test pins every category AND every example as
   separate line-safe anchors — no item can be silently dropped again.

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

* fix(daemon): remove the last key-vocabulary residue from the property-vs-metadata note (MUL-5442)

Round-4 review catch by Elon on #6351: the property-vs-metadata bullet
still read 'free-form scratchpad for run state (pr_url, waiting_on, ...)'
— recommending the ruled-out fields AND calling metadata a home for run
state, in direct conflict with the runtime-bookkeeping ban restored two
sections above. Now reads 'free-form bag for durable custom issue state',
consistent with both the owner ruling and the ban list.

Test side per the review: the broad pr_url anchor narrows to the full
stale-warning phrase (the one sanctioned pr_url reference — a negative
compatibility note, not a write recommendation), and the curation guard
gains the removed residue phrases so neither the vocabulary nor the
run-state framing can creep back.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-04 16:09:05 +08:00
766c7a7fa2 refactor(daemon): retreat deep read semantics to --help, trim BTS prose (MUL-5442) (#6347)
* feat(cli): carry the comment-read contract in comment list --help (MUL-5442)

The --recent flag help gains the MUL-5372 saturation semantics (N caps
threads, not comments; every thread returns uncapped; small issues return
the whole history) and a pointer at the bounded alternative. The --before
help names the stderr cursor labels alongside the response header.

This is the relocation target for the brief's deep read semantics: the
contract follows the flag, and TestIssueCommentListHelpCarriesReadContract
pins it here so the brief-side pointer cannot dangle.

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

* refactor(daemon): retreat deep read semantics to --help and trim BTS prose (MUL-5442)

Unblocked by #6309: the platform no longer recommends --recent anywhere, so
the flag's deep semantics no longer need to ride in every brief.

- Available Commands comment-list entry: keep the signature, the two bounded
  read shapes, and the one-clause saturation warning; per-thread cap detail,
  folding rules, and cursor labels retreat to --help (pinned there by
  TestIssueCommentListHelpCarriesReadContract).
- Workflow step 3: drop the worked examples and the redundant trailing
  pointer; the mandatory two-step read and both motivation pins stay.
- Background Task Safety: tighten connective prose only — every pinned
  phrase and every behavioural constraint is untouched.
- multica-squads SKILL.md quick-start: replace the last remaining --recent
  recommendation on the platform with the bounded scan — same class of fix
  as #6309, found while relocating the warning.

-676 bytes on the standard fixture (16,766 -> 16,090).

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

* fix(cli,daemon): repair the help rendering, restore the BTS orphan scope, complete the squads read (MUL-5442)

Three review catches by Elon on #6347:

1. The --before usage string wrapped the cursor labels in backticks, and
   pflag's UnquoteUsage hijacked the first pair as the flag's value
   placeholder — rendered help showed '--before Next thread cursor' instead
   of '--before string' (same regression class as
   TestLoginTokenHelpOutputRendersCleanly). Labels now use double quotes,
   and the --recent help no longer suggests composing mutually exclusive
   flags ('--roots-only + --thread --tail' -> an explicit two-step read).
   TestIssueCommentListHelpCarriesReadContract now asserts on the RENDERED
   FlagUsages output, pinning '--before string' and the two-step order.

2. The compressed BTS opening said 'anything still running is orphaned',
   which swept externally-owned work (GitHub Actions) into the orphan rule
   the same section later scopes out. Restored: 'any run-owned work still
   active is orphaned', pinned.

3. The squads quick-start's roots-only scan never returns reply bodies,
   where mention triggers and failure reasons usually live. Added the
   bounded drill-down step and the sequence rationale; both reads pinned in
   TestSquadsSkillCoversLeaderRoutingContract, which also guards against a
   regression back to --recent.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-04 14:22:51 +08:00
e92d828f0c refactor(daemon): compress brief prose and demote the sub-issue playbook (MUL-5442) (#6310)
* refactor(daemon): compress brief prose and demote the sub-issue playbook (MUL-5442)

Six static edits to the issue brief, -1,249 bytes on the standard fixture
(17,977 -> 16,728). All edits are identical for every issue run; prompt-cache
byte stability (#6008) is untouched.

- BTS persistent-service bullet: keep the handoff contract (deliverable-only,
  lifecycle detached, readiness verified, best-effort survival), drop the
  operational walkthrough. Pins re-anchored to concepts (durable logs,
  recorded PID, verify readiness).
- BTS CI-ban bullet: keep the command blacklist and the complete-hand-off
  rule, drop the auto-merge/snapshot elaboration.
- Ownership mode: state the identity-forbids clause once on the header
  instead of once per status bullet.
- Delivery invariant: merge the three sub-bullets into the lead paragraph;
  also fixes the stale '(below)' — the per-surface delivery line renders
  above the invariant, not below it.
- Sub-issue Creation: demote the todo/backlog/stage playbook to the
  multica-working-on-issues skill; the brief keeps a one-line flag map plus
  the skill pointer. Skill-side anchors added to
  TestWorkingOnIssuesSkillCoversIssueLoopContracts so the pointer cannot
  dangle.
- Attachments: collapse the two-sentence CLI-fetch intro into one line.

Every pinned behavioral phrase is either carried verbatim or re-pinned to an
equivalent semantic anchor in the same assertion; no assertion is deleted
without a replacement.

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

* fix(daemon): restore the full persistent-service handoff contract (MUL-5442)

Review catch by Elon on #6310: the compressed bullet weakened the contract
two ways — the reply requirement dropped 'logs' from the URL/logs/stop
triple (durable logs alone are unobservable if the user is never told where
they are), and the general ownership/cleanup handle narrowed to a bare PID
(a supervisor/profile-managed service has no single stable PID). Both test
pin sets had been updated to the weakened phrases, which would have made the
regression look legitimate.

Keeps the compressed sentence shape; restores both halves of the contract
and pins them ("cleanup handle such as PID/profile", "URL, logs, and stop
instructions") so they cannot be compressed away again. +38 bytes; the PR
still nets -1,211 against main.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-03 20:05:07 +08:00
3c6176cbcd refactor(daemon): de-duplicate cross-section rules in the runtime brief (MUL-5442) (#6302)
* refactor(daemon): fold four restatements of the end-of-turn rule into one (MUL-5442)

Background Task Safety opened with four bullets that were four views of the
same rule -- do not end the turn with run-owned work outstanding: the general
ban, the "tool says it will notify you" case, the unobservable-result case,
and the "standing by" sign-off. Separating them cost bytes without adding a
distinct behaviour.

Fold them into the leading bullet. Every phrase the behaviour tests pin is
carried over verbatim, including "Do NOT end your turn while background
tasks", "Never background-and-yield", "wait for a future
notification/reminder", "running in the background so you can keep working",
"run the work synchronously instead" and "standing by".

MUL-5442

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

* refactor(daemon): demote workflow-step restatements of delivery, mention and metadata policy to pointers (MUL-5442)

The six workflow steps and the Reply mode block restated policy that already
has a dedicated section. Each restatement is a second place to edit when the
policy changes, which is how they drifted apart in the first place.

Give each rule one canonical home and leave a pointer at the call site:

- delivery ("only a comment reaches the user") -> ## Output; steps 5 and the
  Reply block point at it.
- mention discipline -> ## Mentions. The reply-time phrasing the loop-hardening
  test pins moves into that section rather than being duplicated in the Reply
  block, so the anti-loop signal is unchanged.
- metadata read/write bar -> ## Issue Metadata; steps 2 and 6 point at it
  instead of paraphrasing the bar.

Tests: the metadata scope test pinned the old pointer wording, and the mention
test's comment claimed the sign-off rule lived in the workflow steps. Both are
updated to the new placement; every behavioural phrase they guard is still
asserted, file-wide.

MUL-5442

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

* refactor(daemon): give the comment-read surface and the file-safety rules one home each (MUL-5442)

Three cross-section duplications, each resolved toward the section that owns
the rule:

- comment reads: the workflow step and the Available Commands entry both
  explained what --roots-only / --summary / --thread --tail do. The step keeps
  the two reads it mandates and its anti-stale motive; flag semantics stay in
  Available Commands, which is the single discovery point. The saturation trap
  ("caps THREADS, not comments") and the pagination cursor labels stay put --
  they are load-bearing after MUL-5372 and remain asserted.
- --content-file: the comment add entry restated the guardrail Comment
  Formatting owns; it now names the rule and points there for the rationale.
- workdir path rule (MUL-4252): issue create carried its own copy of the
  stale-file rationale; it keeps the rule and defers the why.
- inbound attachments: trimmed to the pinned rule plus a pointer.

MUL-5442

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

* refactor(daemon): merge the Agent Identity action list, gate squad maintenance, trim the attachment restatement (MUL-5442)

Three follow-ups from review, each re-examined against what the text guards
today rather than against the fact that a test pinned it.

1. Agent Identity enumeration. Instruction Precedence and workflow step 4 were
   added in the same commit (#3802) and each carried its own list of actions
   Agent Identity can forbid -- and the lists disagreed: one named status
   changes, the other named issue create/update and delegation, neither
   contained the other. Merge them into Instruction Precedence, which owns the
   rule. Step 4 keeps only what that section cannot express: a delegation-only
   role stops once its delegation is delivered.

2. Squad maintenance. `multica squad member set-role` shipped to every run,
   including every agent that leads no squad and therefore has no squad whose
   roles it could change. Gate it on IsSquadLeader -- agent configuration, not
   per-run state, so the brief stays byte-stable across runs of one session
   (MUL-5377), the same predicate the workflow already branches on.

3. Inbound attachments. The section restated Output's no-clickable-local-path
   rule verbatim. Keep the framing Output cannot express -- a downloaded
   attachment feels shared but landed in a private workdir -- and point at
   Output for the rule. The delivery test now also asserts the pointed-at rule
   is present, so the pointer cannot dangle.

Brief size, plain issue task: 19,231 -> 18,009 bytes for an ordinary agent
(-6.4%), 20,718 -> 19,759 for a squad leader (-4.6%).

MUL-5442

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-03 19:00:34 +08:00
a54662f30e fix(daemon): provision config-referenced Codex instruction files into task home (#6291)
* fix(daemon): provision config-referenced Codex instruction files into task home

A per-task CODEX_HOME copies the user's config.toml verbatim, so a
model_instructions_file reference survived the move while the file it
names did not. Codex resolves the relative value against CODEX_HOME —
now the task home — and failed loading its configuration before the task
prompt was ever delivered (#6271).

Generalize the existing model_catalog_json provisioning into a keyed
table of path-valued config keys and add model_instructions_file plus its
deprecated experimental_instructions_file alias. Semantics are unchanged
per key: absolute/~ values are left for Codex to read directly, relative
values must stay inside the task home, the copy refreshes on reuse, and a
missing source fails during environment preparation with a diagnostic
naming the key instead of an opaque os error 2 at thread/start.

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

* fix(daemon): root-scope task-home writes for config-referenced Codex files

Review must-fix: filepath.IsLocal only proves the config string has no
lexical '..'. A task home is reused, and the task that ran in it can
replace an intermediate directory of the copy destination with a symlink,
which made the daemon's next MkdirAll/Remove/copy follow that link and
delete or overwrite a file outside the task home.

Do the mkdir, stale-copy removal, and write through os.OpenRoot(codexHome)
so links leaving the task home are rejected (links staying inside it are
harmless), and refuse outright when codexHome itself is a symlink, since
OpenRoot would resolve it before confining anything below. The pre-existing
model_catalog_json path is covered by the same helper.

Also stop reporting every source stat failure as a missing file — a
permission or IO error now says so — and document the source-side symlink
policy plus the stale-copy contract.

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

* test(daemon): pin stale-copy contract when a referenced Codex file is repointed

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

* fix(daemon): bind codex-home root check to the opened handle

Review must-fix: checking the path with os.Lstat and then opening it with
os.OpenRoot leaves a window where the directory is swapped for a link to
somewhere else. OpenRoot resolves the path it is given, so the resulting
root is confined to the wrong tree and every root-scoped write lands
outside the task home without ever escaping its root. Task homes are
reused and Windows cannot confirm descendant cleanup, so a leftover
process that knows its old CODEX_HOME can create that window.

Open first, then prove identity against the handle: compare root.Stat(".")
with a no-follow os.Lstat of codexHome via os.SameFile, and reject a
symlink outright. A swap before the open now fails the check; a swap after
it cannot matter because all writes go through the verified handle.

verifyCodexHomeRoot is split out so the swap is tested deterministically
instead of raced, plus an end-to-end test for a task home that is already
a link to an outside directory.

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

* docs(daemon): scope the codex-home symlink test name and root-handle claim

Review nit: the end-to-end test asserted only that the referenced-file copy
refuses a symlinked task home, but its name read as though the whole
prepare were safe. Rename it accordingly and state in both the test and
openVerifiedCodexHomeRoot that the earlier path-addressed steps of
prepareCodexHomeWithOpts are out of scope, tracked in MUL-5647.

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

---------

Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-08-03 16:23:43 +08:00
e4b6f7a31b MUL-5581: add Qoder CN CLI runtime (#6232)
* feat(agent): add Qoder CN CLI runtime

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

* fix(agent): address Qoder CN review nits

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

* fix(agent): defer Qoder CN version gate

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

---------

Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-31 16:01:53 +08:00
98766cc06a feat(daemon): remove the Linux Codex per-task HOME; default Linux to danger-full-access (MUL-5578) (#6233)
* feat(daemon): default Linux Codex to danger-full-access on the real HOME (MUL-5578)

Linux Codex tasks ran under the `workspace-write` Landlock sandbox with a
generated per-task HOME: the daemon rewrote HOME/XDG_*/npm_config_cache into
`<envRoot>/home`, symlinked a hand-maintained allowlist of seven credential
paths back into it, and granted that directory as a `writable_roots` entry.

That mechanism could not converge. Any host CLI outside the allowlist (aws,
kubectl, gcloud, glab, rclone, cargo, …) started every task as if unconfigured
even though it works in the daemon user's shell, and three open issues asked to
extend it in three incompatible directions (#5636, #5573, #3867). The
containment it bought was also narrower than it looked: workspace-write
restricts writes only — reads and network were already unrestricted, and `.ssh`
was seeded into the task home — so credentials under the real HOME were
readable and exfiltratable regardless.

Linux now runs `danger-full-access` on the daemon user's real HOME and inherited
XDG environment, matching macOS and Windows. The task filesystem boundary is the
boundary the daemon itself runs inside (VM, container, or dedicated Unix user),
which is what the other providers already assumed — Claude Code runs with
`--permission-mode bypassPermissions` today.

This also removes the split-brain the mechanism could enter: the task-HOME
decision read the platform default only, so a `-c sandbox_mode=danger-full-access`
override (which passes arg filtering and wins over config.toml) left a task
running unsandboxed while the daemon still redirected HOME and emitted
writable_roots for a sandbox that was not in effect. With no HOME rewrite there
is only one environment contract left to disagree about.

Removed: prepareTaskHome, prepareCodexSandboxHome, TaskHomeEnv, Env.TaskHome,
both seed allowlists, and the now-unfed WritableRoots plumbing through
codexSandboxPolicy / CodexHomeOptions. Env roots created by older daemons keep
working; their leftover `home/` directory is simply ignored and reclaimed with
the env root.

Task-scoped CODEX_HOME is untouched — it is managed Codex state, not the Unix
HOME. macOS, Windows, and non-Codex providers are unchanged.

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

* docs: document the agent execution security model (MUL-5578)

The docs had no page describing what a task can reach on the machine that runs
it — the sandbox posture was only discoverable from source. Adds one, stating
plainly that tasks run with the full permissions of the daemon user and that
isolation must come from a dedicated Unix user, container, or VM.

Also separates what Multica genuinely isolates (per-task workdir, task-scoped
CODEX_HOME, agent+task-bound API tokens) from what is not a boundary (the coding
tool's own sandbox and approval settings), and links it from step 5 of the
self-host quickstart, where the daemon is first installed.

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

* fix(daemon): address review on the Linux full-access default (MUL-5578)

Docs, both must-fix:

- The Chinese page rendered the product concept as `Agent`; the repo glossary
  in developers/conventions.zh.mdx mandates 智能体. Fixed all nine occurrences.
  ja/ko already used エージェント / 에이전트 and needed no change.
- The security page asserted without qualification that every task runs with
  the daemon user's full permissions and that the Codex filesystem sandbox is
  always off. codexSandboxPolicyForWindows still keeps workspace-write when a
  user explicitly opts into a native windows.sandbox, so an authoritative
  security page contradicted the code. It now states that Multica makes no
  filesystem-sandbox guarantee, names the Windows opt-in as the one current
  exception, and says which combinations sandbox anything is a compatibility
  detail that moves with tool versions. All four locales updated, plus the
  historical callout which now says Linux matches the macOS/Windows *default*.

Both stale comments from the non-blocking notes:

- prepareCodexHome no longer claims to assume workspace-write + network_access;
  it pins GOOS=linux, which now resolves to danger-full-access.
- ensureCodexSandboxConfig's warn-level logging is no longer described as a
  macOS-only fallback; it fires for every danger-full-access resolution.

Adds TestCodexTaskShellEnvInheritsRealHome at the daemon env-assembly layer:
HOME and the XDG base dirs must reach a Codex task's shell tools from the
inherited daemon environment. Verified it fails when that pass-through breaks.
It guards the pass-through, not runTask's decision not to inject a HOME of its
own — that decision is inline in runTask and has no unit seam.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-31 15:42:06 +08:00
abdfd3e28c refactor(skills): make the brief's skill list a names-only index (MUL-5529) (#6207)
* refactor(skills): make the brief's skill list a names-only index (MUL-5529)

Step 3 of MUL-5529. Every runtime CLI discovers the SKILL.md files the daemon
writes and builds its own listing from their frontmatter — verified against 11
locally installed CLIs plus official docs for 5 more. The brief's copy of those
descriptions was therefore the same routing signal paid for twice: measured on
a real task, `## Skills` was 13,295 chars, 40% of the entire brief, against a
16,304-char CLI listing of the same 28 skills.

Now 850 chars for that same set — roughly 3,100 tokens back per brief.

The index itself stays. It is the one skill listing Multica controls; each
CLI's own listing is theirs, and its format — or its existence — can change
with any release.

Three changes:

  - Descriptions dropped from the `## Skills` entries.

  - The per-provider branch is gone. Its fallback told providers outside a
    hardcoded list to read `.agent_context/skills/`, but the only providers
    that ever reached it were grok and traecli, whose files are written to
    `.grok/skills` and `.traecli/skills` and which discover natively. The
    pointer was wrong for everyone it addressed, so removing the branch
    deletes the bug rather than relocating it. This closes MUL-5537.

  - issue_context.md and its quick-create / autopilot variants no longer render
    `## Agent Skills`. That copy duplicated the brief once both were
    names-only, and nothing ever read it: no prompt references the path, and
    grepping the server finds only the writer. `.agent_context/skills/` had the
    same fate for hermes (issue #5242). Quick-create, previously skipped in the
    brief and served only by that unread copy, now gets the brief section like
    every other kind — one index, one place.

Not included: skills carrying `disable-model-invocation` are still written to
disk for every provider. The plan assumed that key needed provider-specific
handling for everything except claude; probing the installed CLIs shows 9 of 11
honor it, and only opencode and hermes do not. The remaining question is
narrow and a genuine product tradeoff — withholding the file honors the
author's intent but also removes explicit invocation — so it is left to a
separate decision rather than folded in here.

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

* docs(skills): align stale comments with the names-only brief contract (MUL-5529)

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Steve Jobs (Multica Agent) <[email protected]>
2026-07-31 13:27:50 +08:00
5199278780 fix(skills): give every skill one name across brief, directory, and frontmatter (MUL-5529) (#6189)
* fix(skills): give every skill one name across brief, directory, and frontmatter (MUL-5529)

A skill could answer to three different names at once. The runtime brief listed
`AgentSkillData.Name` verbatim — a workspace skill's human display name ("PR
review") — while the invocable identity on disk is the sanitized slug
(`pr-review`). Separately, ensureSkillFrontmatter returned valid upstream
frontmatter untouched, so a SKILL.md could declare `name: multica-dev-workflow`
inside a directory called `multica-git-workflow`.

That last divergence is the sharp one: runtimes disagree on which field
identifies a skill. Claude routes on the directory name, OpenCode on the
frontmatter `name`. So the same skill is invocable under different names
depending on where it runs, and the brief's instruction to use "only names from
the listing" pointed at names that resolve nowhere.

The slug is authoritative: it is what lands on disk, it derives from the name
users see in the product, and it is the only value with a uniqueness guarantee
(allocateCollisionFreeSkillDir). A frontmatter `name` is author-supplied and two
imported skills may both claim the same one.

- modelVisibleSkills normalizes Name to the slug. All four model-visible
  listings (runtime brief + the three issue_context renderers) already route
  through it, so they cannot drift apart.
- ensureSkillFrontmatter rewrites `name` to the allocated slug and keeps every
  other key byte-identical, so deliberately shaped upstream frontmatter still
  survives. The rewrite follows the collision fallback slug too.
- Name matching is now top-level only. An indented `name:` belongs to a nested
  mapping; treating it as the skill's identity both missed that the block had no
  top-level name and would have spliced a top-level key into the nested one.

Known gap, tracked separately: the listings render the natural slug, so a
collision fallback to `<slug>-multica` still leaves the brief naming the user's
skill. Closing it needs the allocated slug threaded back from Prepare, which the
renderers cannot reach without giving up the byte-identical-brief guarantee.

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

* fix(skills): cover multi-line name values and in-batch slug collisions (MUL-5529)

Two holes in the name-unification change, both found in review, both
reproduced before fixing.

1. setFrontmatterName replaced only the `name:` line, not the rest of the YAML
   value. A value may continue onto indented lines — `name: >-\n  upstream`,
   multi-line plain scalars, wrapped quoted scalars — so the continuation
   survived and YAML folded it into the new value: the block parsed as
   "my-slug upstream-name", not "my-slug". The directory == frontmatter-name
   invariant this change set exists to establish was still broken, just less
   visibly. frontmatterNameSpan now covers the whole value.

   As a side effect this also fixes `name:` with the value entirely on the
   following line, which previously read as "no name" and got a second
   top-level `name` injected above it — two `name` keys, which strict loaders
   reject outright.

2. The listings derived slugs with sanitizeSkillName alone, which is not
   injective: "A B" and "A-B" both reduce to "a-b". writeSkillFiles resolved
   that at write time, so the second skill landed in `a-b-multica` while both
   were listed as `a-b` — the second skill had no invocable name and the model
   was pointed at the first. This needed no user-installed skill and no
   local_directory; two such skills bound to one agent reproduce it in a clean
   workdir. resolveSkillSlugs now deduplicates the batch up front and both the
   listings and the writer derive from it, with skillSlugCandidate shared so
   the in-memory and filesystem allocators cannot disagree on the suffix
   sequence.

   Slugs are allocated over the unfiltered batch: hidden
   (disable-model-invocation) skills are still written to disk and still
   consume a slug, so filtering first would shift every later suffix.

Filesystem-dependent collisions against user-installed directories remain out
of scope and are still tracked in MUL-5550.

Regression tests assert the parsed YAML value rather than the output text —
the first line looked correct in every one of these cases — and set-equality
between listed names and the directories actually written. Both were confirmed
to fail against the previous implementation.

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

* fix(skills): bound the frontmatter name value with the YAML parser (MUL-5529)

Review round two found a valid multi-line name the indentation rule still
mangled. A quoted scalar may wrap onto a line at the *same* indentation as its
key:

    name: "upstream
    continued"

The rule stopped at the key's line, stranding `continued"` and producing
invalid YAML. Probing the parser showed the same holds for flow collections
(`name: [a,\nb]`, `name: {a: 1,\nb: 2}`), so this was not a quoting special
case: indentation simply does not bound a YAML value, and no amount of
patching the heuristic would have made it one.

Value extent now comes from yaml.v3's own line numbers. frontmatterNameValueSpan
locates the `name` key node and ends the span where the next top-level key
begins, stepping back over blank lines and unindented comments so they survive
the rewrite. Unindented is the operative word: block scalar content is always
indented, so an unindented `#` can only be a comment, while `  # text` inside a
block scalar is value and stays in the span.

Detection stays lexical, in lexicalFrontmatterNameSpan. It runs on malformed
blocks too, where there is no parse to consult, and only decides which branch
to take.

setFrontmatterName now re-parses its own output and returns verified=false
unless `name` really is the slug; ensureSkillFrontmatter then routes to the
existing re-synthesis path rather than emitting a block it cannot vouch for.
This adds no new fallback — it feeds an already-present one. The invariant is
the whole point of the change, so an unprovable rewrite is worth less than a
reformatted block: with the span deliberately broken, output stays valid YAML
carrying the right name and loses only upstream formatting.

Tests extend the table to same-indent single/double quoted scalars, flow
sequences and mappings, and name-as-last-key, and now assert the *input* parses
so a case cannot pass by silently taking the re-synthesis path. Two more cover
comment survival and a `#` line inside a block scalar name. All four
same-indent cases fail against the previous implementation.

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

* fix(skills): preserve policy keys when the surgical name rewrite fails (MUL-5529)

Review round three: the post-condition check added last round was routing valid
YAML into bare re-synthesis, and re-synthesis emits only name and description.

An anchor on the name value is one way to get there. Replacing the line drops
`&skill_name`, so `description: *skill_name` no longer resolves, the check
correctly rejects the rewrite — and the block was then rebuilt as just:

    name: my-slug

`disable-model-invocation: true` went with it. That key is the author's
instruction that a runtime must not surface the skill on its own, and the
SKILL.md we write is what native discovery reads, so losing it advertises a
skill that was deliberately hidden. Confirmed on the written output:
skillDisablesModelInvocation went from true to false. That is a semantic
regression, not the formatting loss the fallback was justified by.

The two failure modes are now separate:

  - invalid YAML → re-synthesize, unchanged; there is nothing to preserve.
  - valid YAML, rewrite unprovable → renameFrontmatterNameViaNode rebuilds the
    block from the parsed node with `name` set to the slug, keeping every other
    key. Formatting normalizes; semantics survive.

The anchor is deliberately kept on the name node. Dropping it is what
invalidates a document that aliases it; keeping it means the alias resolves to
the slug — that value changes, but the document still loads and every policy
key is intact. The rebuilt block is re-parsed and checked like the surgical
path, so an unprovable result still falls through to re-synthesis.

Regression test asserts name, disable-model-invocation, and a custom key all
survive, and that the written file still reads as hidden to
skillDisablesModelInvocation. It fails without the new path.

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

* fix(skills): materialize aliases before renaming an anchored name (MUL-5529)

Round three kept the anchor on the name node so aliases would not dangle. That
avoided an invalid document but created a worse one: every alias resolves
through the anchor, so renaming the anchored value silently rewrote whatever
those fields meant.

    name: &shared "true"
    disable-model-invocation: *shared

skillDisablesModelInvocation went true -> false across the rewrite. Same
skill-exposing regression as round three, reached by keeping the key instead of
dropping it — which is the lesson: preserving a key is not the invariant,
preserving each key's resolved value is.

Aliases pointing at the name node are now materialized to the value they
resolved to *before* the rename, and the anchor is dropped afterwards as
unreferenced. Ordering matters: the clones are taken first, so they capture the
original value rather than the slug. Copies are per-alias, since sharing one
node would make the encoder re-emit an anchor/alias pair.

Nested aliases are covered by walking the whole document, not just the top
mapping.

Tests: three shapes (alias carrying the policy value, alias nested in another
mapping, two aliases of one anchor) each assert the fixture starts hidden and
stays hidden, that no anchor or alias survives, and that name is the slug. The
round-three test now also pins its aliased `description` to the pre-rename
value instead of merely tolerating the slug. All four fail without the change.

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

* fix(skills): let the parser decide whether a name key exists (MUL-5529)

The branch that chooses between "rewrite the name" and "inject a name" was
still gated on a lexical scan, which only recognizes a bare `name:` carrying a
value on the same line. Two valid spellings therefore read as nameless:

    "name": upstream        # quoting is syntax, not identity
    name:                   # a key with no value is still the key

Both got a second `name` injected above the existing one, and a duplicate
mapping key is rejected outright — `mapping key "name" already defined`. The
skill does not end up misnamed, it fails to load. Single-quoted keys have the
same problem.

For valid YAML the parsed top-level mapping now answers the question, so any
spelling of the key routes to the rewrite. The lexical scan is confined to the
invalid-YAML branch, where there is no parse to consult and it is only choosing
between re-synthesis and injection. Nesting still reads as absent: a `name`
under another mapping is not the skill's name, so one is added.

Once past the gate the existing paths handle both shapes unchanged, since
frontmatterNameValueSpan already bounds the entry by node line numbers rather
than by how the key is written.

Also tightens the alias tests per review: the nested and two-alias cases now
assert `meta.inner` and `other` still resolve to the anchor's original value,
not merely that visibility survived. The invariant is every key's resolved
value, so every alias should be pinned, not just the one that gates hiding.

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

* fix(skills): detect quoted and valueless name keys in malformed frontmatter (MUL-5529)

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Steve Jobs (Multica Agent) <[email protected]>
2026-07-30 21:21:13 +08:00
e45a8f6c12 fix(daemon): scan comment roots before bulk reads in agent catch-up (MUL-5372) (#6093)
* fix(daemon): scan comment roots before bulk reads in agent catch-up

The mandatory step-3 catch-up in the issue runtime brief asked for
`--recent 10`. `--recent N` caps THREADS, not comments: each returned
thread carries its root plus every descendant with no depth bound, so on
an issue with fewer than N root threads it returns the entire comment
history. Because the step is mandatory and fires on every run, every
reply turn re-read the whole issue -- and on comment-triggered turns it
duplicated the bounded thread read the per-turn message had already
pointed at, so the same bytes were fetched twice.

Lead the step with `--roots-only --summary` instead: every top-level
thread with reply_count and last_activity_at, contents clipped. That
keeps the property the step exists for -- the agent still sees every
thread that exists, so it cannot act on stale context -- and makes the
drill-down into `--thread <id> --tail 30` explicit. `--recent 10` stays
documented for when several complete threads really are needed, now with
its saturation semantics spelled out.

Measured on a live 2-thread issue: 21,249 -> 1,518 bytes for the
mandatory read (-93%), and the duplicate 11,082-byte thread read is gone.

The brief stays byte-identical across runs of a session (MUL-5377): the
new text interpolates only the issue id, no per-run state. The three
per-turn pointers that express the same rule move with it so the two
layers cannot drift.

MUL-5372

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

* refactor(daemon): keep comment-read flag semantics in one place

The previous commit fixed the payload shape but restated the read surface
in four places: the workflow step, both per-turn prompt fallbacks, and the
cold-start hint each explained what `--recent 10` does. `## Available
Commands` is already the brief's single discovery point for these flags,
and `TestInjectRuntimeConfigStaticCatchUp` pins it as such -- so those
restatements were duplicated reference text, and the per-turn ones were
paid on every turn rather than once in the cached prefix.

Move the `--recent N` saturation warning into the `comment list` line in
Available Commands, next to the flags it qualifies, and add `--roots-only`
and `--summary` to that signature so the bounding options are discoverable
where an agent already looks. Workflow steps and per-turn hints now name
only the reads they actually want run.

Per-turn prompt sizes: assignment 1170 -> 749 bytes (-36%), cold-start
comment turn 1550 -> 1355 (-13%). Step 3 is 1065 bytes and no longer
carries a ready-to-paste bulk read.

MUL-5372

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

* docs(daemon): address review nits on comment-catchup change

Three cosmetic follow-ups from review:

- `--recent N` saturation warning said it hands back "the entire
  history"; resolved threads are still folded by default on that read, so
  say so.
- Rename two tests whose names still advertised `--recent` after their
  assertions stopped mentioning it, plus the one added in this branch
  whose name referenced a bulk read the step no longer contains:
  MentionsRecent -> ScansRootsFirst, ScansRootsBeforeBulkRead ->
  ScansRootsFirst.
- Fix the stale doc comment that still described the mandatory read as
  bounded to "the recent active-thread window".

No behavior change.

MUL-5372

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-29 16:08:55 +08:00
4eb80c8b77 fix(daemon): keep the runtime brief byte-stable across triggers (MUL-5377) (#6021)
* fix(daemon): keep the runtime brief byte-stable across triggers (MUL-5377)

Claude Code loads the runtime brief (CLAUDE.md / AGENTS.md) into messages[0],
ahead of the entire conversation. A cache breakpoint is all-or-nothing, so a
single differing byte there invalidates the prompt cache for the whole history
on every `--resume`. InjectRuntimeConfig rewrites that file on every run and
interpolated nine per-run values into it, so in practice the cache was thrown
away on the first comment that landed on any issue.

Measured on one issue over three runs: run 1 (cold) spent 89.9k cache-write
tokens building 105k of context; runs 2 and 3 each spent ~425k re-creating a
prefix they should have read. 842k of 946.5k cache-write tokens (89%) went into
re-creation, with only tools[]+system[] surviving each resume (a constant
18,085 tokens both times).

Fix: the brief now carries only what is stable for the lifetime of a resumed
session, and per-run state travels in the per-turn user message, which is
appended after the cached prefix.

- Merge kindCommentTriggered + kindAssignmentTriggered into kindIssue, and stop
  reading TriggerCommentID in classifyTask. The brief can no longer diverge by
  trigger type structurally, rather than by convention.
- Replace writeWorkflowComment/writeWorkflowAssignment with one writeWorkflowIssue
  that routes on the per-turn message. The mode-specific status rules live inside
  their own mode block, so "own the status arc" and "do not touch the status"
  can never be read as unarbitrated peers.
- Move Task Initiator, Session Continuity Notice and Connected Apps out of the
  brief into BuildPrompt via BuildTaskInitiatorBlock / SessionContinuityNotice /
  BuildConnectedAppsBlock.
- Drop TriggerCommentID, TriggerThreadID, NewCommentsSince, NewCommentCount,
  PriorSessionResumed and CommentReplyTargets from the brief; BuildPrompt already
  emitted all six from the same helpers, so this is de-duplication.
- Set PriorSessionResumeUnavailable on `task` as well as `taskCtx` in both local
  resume gates, or the notice would silently vanish on exactly the failure path
  it exists to disclose.

Tests: TestInjectRuntimeConfigByteIdenticalAcrossTriggers renders the brief
across nine per-run variants (trigger type, differing comment/thread ids, resume
delta, resume-unavailable, cross-thread fan-out, member/agent initiator,
connected apps) for two providers and requires bytes.Equal, with a non-vacuity
guard so it cannot pass on a function that ignores its input. Daemon-side tests
assert the moved sections still reach the agent through the per-turn prompt.

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

* fix(daemon): route the issue workflow on an explicit turn-mode marker

Review follow-up on the mode router. The brief said Reply mode applies when
the per-turn message "opens with a [NEW COMMENT] block", but buildCommentPrompt
writes two paragraphs before that block and only emits it when
TriggerCommentContent is non-empty. Two ways to get it wrong:

- The message never literally opens with the block, so the router's own
  wording did not match the prompt it describes.
- A comment-triggered run with an empty comment body — or an older server that
  does not send one — emitted no block at all. An agent following the brief
  would fall through to Ownership mode and change the issue status on a turn
  whose rule is "do NOT change the issue status".

BuildPrompt now emits an unconditional `**Turn mode: Reply.**` /
`**Turn mode: Ownership.**` line from the same branches it uses to pick a code
path, and the brief routes on that marker. Brief and prompt can no longer
disagree about the mode, because the value that selects the path also states
it. The router also names a safe fallback (treat an unlabelled turn as Reply
mode and leave the status alone).

Tests: TestTurnModeMarkerAlwaysPresent covers comment-triggered with and
without comment content, plus both assignment shapes;
TestTurnModeMarkerAbsentOnIssuelessKinds keeps the marker off chat /
quick-create / autopilot; TestBriefModeRouterMatchesPromptMarkers fails if the
brief ever describes a marker the prompt does not emit.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-28 16:00:16 +08:00
3c6bebaff0 MUL-5274: allow explicit persistent service handoffs (#5895)
* fix(runtime): allow explicit persistent service handoffs

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

* fix(runtime): resolve review ambiguities in persistent-service handoff wording

Address MUL-5274 review findings on #5895:

- Drop the "The rules above apply only to work owned by the current run"
  scoping sentence: with the persistent-service exception inserted above
  it, it would have swept in work that is precisely no longer run-owned
  after handoff. The external-systems bullet carries the boundary on its
  own, and both pin tests now reject any "The rules above" reintroduction.
- Replace "detach it" (skill-level mechanism) with the lifecycle
  contract: hand off only once the service no longer depends on this run.
- End the negative-boundary bullet with "the CI-specific rules below
  still apply" instead of "must be collected before exit", which
  misread as license to start CI polling and collect it.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-24 18:21:28 +08:00
a3fe6d91dd MUL-5150: add project context to Chat (#5765)
* feat(chat): add project context

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

* fix(chat): resolve MUL-5150 review blockers

- Renumber project-context migrations to unique prefixes after current main:
  206_chat_session_project -> 212 (column), 207_chat_session_project_index ->
  213 (concurrent index). 206/207 collided with 206_agent_disabled_runtime_skills
  and main's 207-211 client_usage_daily set.
- Add the 4 missing chat input.project_context keys to ja/ko locales so the
  locale parity test passes (en/zh-Hans already had them).
- Lock the project-context control while a send is in flight (isSubmitting),
  not just while the agent is running. A brand-new chat creates its session
  lazily during send bound to the project at click time; switching project
  mid-send would create the session against the stale project and clear the
  editor as if the send landed on the new selection. Add a regression test.

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

* fix(chat): complete project context handling

* fix(chat): pin fresh chat to open session's agent on project switch

Switching an existing session to a different project opens a fresh chat but
only cleared the active session, dropping selection back to the stored
`selectedAgentId`. When that preference was stale (open session belongs to
agent B while the persisted pick is still agent A), the lazily-created session
and its first send bound to the wrong agent (agent A).

Extract the project-switch decision into a shared `planProjectContextChange`
pure helper in use-chat-controller.ts and route both chat surfaces (the chat
tab controller and the floating ChatWindow) through it, so the fresh chat is
pinned to the open session's agent and the rule cannot drift between the two
copies. Add a dual-entry regression test (pure-fn guard + controller
integration) covering the stale selectedAgentId case.

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

* chore(ci): re-trigger required checks on latest head

The prior push updated the branch ref but GitHub did not emit a pull_request
synchronize for it (PR head-sync lag), so CI/Mobile Verify never ran on the
commit carrying the stale-agent project-switch fix. Empty commit to force a
fresh synchronize on a head that includes it.

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

* fix(chat): renumber project migrations to 213/214 after main added 212

Current main added 212_agent_service_tier; the PR's 212/213 chat migrations
collided with it on the merge ref, failing TestMigrationNumericPrefixesStay
UniqueAfterLegacySet. Merge current main and move the chat column migration to
213 and the concurrent index migration to 214 (column before index preserved).

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

* fix(chat): lock ProjectPicker clear control during send (keyboard path)

The send-pending lock only put pointer-events-none on the wrapper, which
blocks the mouse but leaves ProjectPicker's inline clear button in the tab
order — a keyboard user could Tab to "Remove from project" and press Enter
mid-send, detaching the project after the lazily-created session already went
out with the old one (reopens the mid-send retarget path via keyboard).

Add an explicit `disabled` capability to the shared ProjectPicker that locks
the trigger, the menu (forced closed), and the inline clear button (disabled +
out of the tab order). Defaults to false, so issue/create/autopilot callers
keep their hover/keyboard clear. ChatInput passes disabled while the project
selection is locked.

Tests: real-ProjectPicker regression (keyboard activation of the clear control
is inert when disabled; still works when enabled) + ChatInput wiring assertion
that the picker is disabled mid-send.

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

---------

Co-authored-by: Lambda <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: Walt <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
Co-authored-by: Naiyuan Qing <[email protected]>
Co-authored-by: NevilleQingNY <[email protected]>
2026-07-24 11:30:27 +08:00
e2ce3a2da8 MUL-5223 fix(runtime): forbid blocking on external CI in the runtime brief (#5840)
* fix(runtime): forbid blocking on external CI in the brief (MUL-5223)

The external-work boundary added in #5803 did not stop agents from
waiting on GitHub Actions. Two holes: the section's only concrete
"how to wait" example was a blocking foreground call, which is exactly
the shape of `gh pr checks --watch`; and the "unless acceptance
criteria require it" escape was satisfied by the repo's own merge
requirement that CI be green.

Name the banned tool shapes, allow a single non-blocking status
snapshot, deny branch protection as an acceptance criterion, and give
the replacement hand-off phrasing (local test result + PR link).

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

* fix(runtime): scope the CI-wait ban so the explicit exception stays executable (MUL-5223)

Review feedback on #5840:

- The ban read as absolute ("Blocking on external CI is never part of
  your deliverable") while the next bullet allowed waiting when the
  task explicitly asks for the CI result, leaving no way to satisfy
  both. The ban is now scoped to "unless the explicit exception below
  applies", and the exception names the one executable shape: a single
  foreground blocking watch inside the same turn.
- `gh pr merge --auto` enables auto-merge and returns; it is not a
  wait. Only waiting for it to land is banned.

Both hard-pin tests now also pin the exception so it cannot be dropped
or re-absolutised.

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

* polish(runtime): group Background Task Safety into run-owned and external-CI clusters (MUL-5223)

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

* polish(runtime): cut redundant phrasing from the external-CI cluster (MUL-5223)

The cluster said "report and finish" three different ways and carried
two rhetorical tails. Fold the delivery template into the post-push
playbook bullet, tighten the merge-gate denial, and drop filler.
5 bullets -> 4, -36 words, every behavioral fact and test pin intact.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-23 18:38:51 +08:00
Jiayuan Zhang 2c749ddf9f fix(runtime): clarify external background work (#5803) 2026-07-23 02:43:17 +08:00
fbe00ca164 fix(daemon): run Codex unsandboxed on Windows to stop reject-by-policy (MUL-4957) (#5672)
* fix(daemon): run Codex unsandboxed on Windows to stop reject-by-policy (MUL-4957)

Windows has no Landlock/Seatbelt-equivalent filesystem sandbox that the
daemon configures, so the per-task `sandbox_mode = "workspace-write"` it
wrote was unenforceable. Worse than having no sandbox, it pushed Codex
into rejecting non-safe mutation commands "by policy": `multica issue
create` fails with "was rejected by policy" because Codex can neither
sandbox the command nor (under approval_policy = "never") escalate it to
the daemon's auto-approver, so the request never reaches the approver.

Mirror the existing macOS fallback and give Windows danger-full-access so
those commands run. Also generalize the danger-full-access warn log so it
no longer hardcodes "on macOS" and only surfaces the macOS-specific
upgrade hint on macOS (new codexSandboxPolicy.Hint field).

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

* fix(daemon): correct Windows sandbox rationale and respect user windows.sandbox (MUL-4957)

Addresses two review must-fixes on #5672:

1. Correct a false security fact. The comments and log Reason claimed
   Windows has no filesystem sandbox backend. Codex 0.144.5 does ship a
   native Windows sandbox (windows.sandbox = "unelevated"/"elevated"); it
   is experimental with open upstream reliability bugs, so the daemon
   defaults to danger-full-access as a deliberate compatibility choice.
   Enabling the native sandbox is tracked as separate follow-up work.

2. Stop silently downgrading users who opted into isolation. The fallback
   was unconditional. Add codexSandboxPolicyForConfig: on Windows an
   explicit windows.sandbox = unelevated|elevated keeps workspace-write so
   Codex enforces task isolation with the user's chosen backend;
   danger-full-access applies only when windows.sandbox is absent,
   disabled, or unparseable. This is also the branch point for a future
   native-sandbox rollout (flip the default; callers unchanged).

Adds fixture tests locking the priority (user opt-in kept vs. unconfigured
fallback) plus predicate coverage for codexSandboxPolicyForConfig.

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

* fix(daemon): fail closed on undecidable Windows sandbox config, honor -c windows.sandbox (MUL-4957)

Second review round on #5672. Two must-fixes.

1. Undecidable config no longer fails open. The old bool detector collapsed
   "unparseable / invalid value / failed copy" into "unconfigured" and then
   loosened to danger-full-access. Replaced with a tri-state
   (absent/native/undecidable): only exact-lowercase unelevated|elevated (the
   sole values Codex accepts — verified: any other value makes Codex refuse to
   load the config) counts as native; any other present value, unparseable
   TOML, a read error, or a missing per-task config when a shared
   ~/.codex/config.toml exists (i.e. the copy failed) is undecidable and fails
   closed to workspace-write — it never loosens — logged at error level.

2. windows.sandbox set via `-c`/`--config` custom args is now honored. Such
   args never land in config.toml, so config-only detection silently
   downgraded those users' isolation. The effective Codex args (daemon
   defaults + profile-fixed + per-agent custom_args) are threaded through
   PrepareParams/ReuseParams/CodexHomeOptions into the sandbox decision and
   scanned for a windows.sandbox override (inline, two-token, quoted, spaced;
   last-wins).

Also drops issue-status-bound source comments (openai/codex#24098 has since
closed). Adds unit coverage for config/args classification, the fold
precedence (undecidable > native > absent), and the copy-failed fail-closed
path.

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

* fix(daemon): fail closed on config-sync errors and honor shell-quoted -c windows.sandbox (MUL-4957)

Round-3 review must-fixes:

1. resolveWindowsSandboxState now takes the config.toml sync error and a
   tri-state shared-config presence instead of re-stat-ing inside. A failed
   sync (stale/absent per-task copy) or an un-stat-able shared source is
   undecidable and keeps workspace-write, closing the fail-open where a failed
   sync was read as "unconfigured". Splits IO from the decision so the paths
   are unit-testable without faulting the filesystem.

2. The Windows sandbox decision consumes agent.NormalizeCodexLaunchArgs (the
   shared helper buildCodexArgs now uses) so a shell-quoted -c windows.sandbox
   opt-in is normalized identically to launch, instead of being missed by a
   raw-token scan and silently downgraded.

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

* fix(daemon): abort when the Codex sandbox block cannot be written (MUL-4957)

Round-4 review must-fix: ensureCodexSandboxConfig failures were warn-and-continue,
so a computed fail-closed workspace-write policy could stay only in memory while
config.toml kept a stale danger-full-access from a prior run — the decision
failed closed but the effective config failed open.

prepareCodexHomeWithOpts now returns the error, which blocks startup on both
paths: fresh Prepare fails the task, and Reuse leaves env.CodexHome unset, which
configureCodexTaskShellEnvironment already refuses to start.

Regression covers the full reuse scenario (stale danger-full-access + failed
config sync + failed managed-block write); it fails with "got nil" without the
fix.

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

---------

Co-authored-by: Bohan-J <[email protected]>
Co-authored-by: multica-agent <[email protected]>
Co-authored-by: J <[email protected]>
2026-07-21 15:18:44 +08:00
Bowser f8bf6cd8b9 feat(runtime): add Qwen Code runtime (MUL-5015)
Merge approved PR #5666.
2026-07-21 14:55:08 +08:00
5664c0d755 MUL-4874: fix(execenv): seed Codex model cache in task homes (#5449)
* fix(execenv): reuse Codex startup caches

* fix(execenv): preserve task-local Codex model cache

* fix(codex): bind model cache to provider config

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

---------

Co-authored-by: Eve <[email protected]>
Co-authored-by: multica-agent <[email protected]>
2026-07-16 17:13:52 +08:00
54fd29ebdd MUL-4383 fix(daemon): stop routing CodeBuddy skills/memory through Claude's .claude paths (#5224)
* fix(daemon): stop routing CodeBuddy skills/memory through Claude's .claude paths

CodeBuddy Code is a Claude Code fork but ships its own native config
directory (~/.codebuddy, .codebuddy/) with its own memory filename
(CODEBUDDY.md). It only reads .claude/skills or CLAUDE.md if a user
manually symlinks/copies them during migration
(https://www.codebuddy.ai/docs/cli/troubleshooting#migrating-from-claude-code).

Multica's daemon/execenv code treated "codebuddy" as an alias for
"claude" in three places, so skills synced by Multica landed in
.claude/skills/ and CLAUDE.md — paths the default CodeBuddy install
never reads — instead of ~/.codebuddy/skills, .codebuddy/skills, and
CODEBUDDY.md as documented at
https://www.codebuddy.ai/docs/cli/codebuddy-dir and
https://www.codebuddy.ai/docs/cli/skills.

Split the "claude", "codebuddy" switch cases in:
- daemon/local_skills.go (user-level local skill discovery/import)
- daemon/execenv/context.go (per-task skill materialization)
- daemon/execenv/runtime_config.go (runtime brief target file)

Added regression tests locking in the new paths and updated the
install-agent-runtime / providers docs (all 4 locales) that had
documented the old .claude/skills behavior.

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

* test(daemon): cover CodeBuddy sidecar hygiene and lifecycle

Address PR #5224 review feedback: exclude CODEBUDDY.md/.codebuddy from
repo-cache worktrees, extend sidecar lifecycle matrices to codebuddy, and
make the local-skills CodeBuddy test exercise a true same-key collision.

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

---------

Co-authored-by: Cursor <[email protected]>
Co-authored-by: Eve <[email protected]>
2026-07-15 16:47:15 +08:00