mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
main
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]>
|
||
|
|
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]>
|
||
|
|
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]>
|
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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 |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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 (
|
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
586a624aa5 | fix(cli): isolate task profile state (#6407) | ||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]>
|
||
|
|
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]>
|
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
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]> |
||
|
|
2c749ddf9f | fix(runtime): clarify external background work (#5803) | ||
|
|
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]> |
||
|
|
f8bf6cd8b9 |
feat(runtime): add Qwen Code runtime (MUL-5015)
Merge approved PR #5666. |
||
|
|
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]> |
||
|
|
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]> |