mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-06 05:49:12 +02:00
agent/lambda/7a73e08e
148 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
18a58e80c0 |
MUL-3316: fix(execenv): switch agent prompt to --content-file to prevent heredoc flag swallowing (#4182) (#4191)
* fix(execenv): switch agent prompt to --content-file to prevent heredoc flag swallowing (#4182) The Linux/macOS reply template recommended --content-stdin with a quoted HEREDOC. That pattern is safe for the trivial single-flag comment-add case that BuildCommentReplyInstructions emits, but as soon as a model wraps extra flags around the heredoc on multica issue create / update — assignee, project — the bash heredoc/flag boundary is fragile in two ways the model cannot see: - A 'BODY \\' terminator with a trailing token is not recognised as the heredoc end, so flag lines after it are swallowed into the description (OXY-78: residual flag text leaked into the description, command exit 0). - A clean terminator turns the trailing '--assignee ...' line into a separate failing shell statement, while the create itself already exited 0 with no assignee (OXY-76: assignee silently dropped, no residual text). In both cases the CLI never receives the swallowed flags, the API request omits the fields, and the daemon has no visibility. The created issue lands with assignee_id: null / project_id: null. This commit: * Switches the Linux/macOS branch of BuildCommentReplyInstructions to --content-file with a 3-step recipe (write file, post, rm) so the body never reaches the shell and all flags live on one shell-token line. There is no heredoc boundary for flags to leak across. * Adds a parallel cleanup step (Remove-Item) to the Windows branch so the cross-platform template is one shape. * Rewrites the runtime_config.go ## Comment Formatting non-Windows section to mandate --content-file and explicitly ban --content-stdin HEREDOC for agent-authored comments, citing #4182. * Reorders the Available Commands menu lines for issue create / update / comment add to put --content-file / --description-file ahead of the stdin variant and add a per-line note pointing at #4182. * Updates and renames the affected tests (TestBuildCommentReplyInstructionsCodexLinux, TestBuildCommentReplyInstructionsNonCodexLinux, TestInjectRuntimeConfigLinuxCommentFormattingEmphasizesFile, TestInjectRuntimeConfigIssueMetadataCodexFormattingUnchanged) so the new file-first contract is pinned and the old HEREDOC mandate is in the banned-strings lists. This converges Linux/macOS with the long-standing Windows file-only path, so the cross-platform guidance is now one shape. It also strictly improves on the previous MUL-2904 guardrail by eliminating shell exposure of the body entirely (no body ever reaches the shell, so backtick / $() / $VAR substitution cannot corrupt it). Closes GitHub multica-ai/multica#4182. No CLI or backend changes — --content-file / --description-file already exist. Co-authored-by: multica-agent <github@multica.ai> * docs(prompt): correct stale BuildPrompt comment to file-first (#4182) --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: CC-Girl <cc-girl@multica.ai> |
||
|
|
34d4cd3a28 |
feat(openclaw): support connecting to existing OpenClaw gateway (#3260) [MUL-3158] (#3664)
* feat(openclaw): support connecting to existing OpenClaw gateway (#3260) When the daemon host is a lightweight dev machine or CI coordinator, the heavy agent work (LLM inference, code execution, tool use) often belongs on a more powerful remote server already running an OpenClaw gateway. Multica historically hard-coded `openclaw agent --local`, forcing every turn to execute in-process on the daemon host. This change adds an opt-in gateway routing mode controlled per-agent via `runtime_config`: { "mode": "gateway", "gateway": { "host": "...", "port": 18789, "token": "...", "tls": false } } - Backend: ExecOptions gains OpenclawMode + OpenclawGateway; buildOpenclawArgs drops `--local` when mode == "gateway". Per-task openclaw-config.json wrapper pins gateway.{host,port,auth.{mode,token},tls} so users do not need to edit the daemon host's `~/.openclaw/openclaw.json` to point at a different endpoint. - Daemon: AgentData carries the raw runtime_config; decoding is fail-soft (malformed JSON falls back to local mode rather than blocking dispatch). - API: gateway.token is masked to "***" on every GET; PATCH replays the sentinel back, and the update handler restores the persisted token so the round-trip never destroys the secret. Defense-in-depth masking on WS broadcasts, plus String/MarshalJSON masking on the in-memory struct to block stray `%+v` / json.Marshal leaks. - UI: openclaw-only "Routing" tab on the agent detail page with mode selector + structured endpoint form. Token uses a "saved — submit a new value to rotate" UX and matching backend preserve hook. Empty `runtime_config` keeps the historical embedded behaviour, so existing agents are unaffected. * fix(openclaw): address #3664 review — drop dead gateway field, gate pin on mode Per Bohan-J's review: - Remove the dead ExecOptions.OpenclawGateway field (+ its String/MarshalJSON and the daemon.go construction block). It carried the plaintext bearer token but was never read — buildOpenclawArgs only consumes OpenclawMode and the live gateway path runs through execenv.OpenclawGatewayPin — so this narrows the secret's footprint. - Gate the gateway pin on mode=="gateway" in decodeOpenclawRuntimeConfig: a {"mode":"local","gateway":{...,"token"}} payload no longer writes the token into the 0o600 per-task wrapper that --local makes openclaw ignore. - Warn on an unrecognized non-empty mode (e.g. "gatway") instead of silently falling back to local. - Run preserveMaskedGatewayToken in CreateAgent too, so a literal "***" at create time can't persist as a real bearer token. - Document the gateway host:port trust boundary (SSRF note for shared daemon hosts). Adds regression tests for the local-mode pin drop and the unknown-mode warning. |
||
|
|
f415099c4a |
MUL-3263: support managed MCP config for Cursor (#4081)
* feat: support managed MCP config for Cursor Co-authored-by: multica-agent <github@multica.ai> * fix: address Cursor MCP review feedback Co-authored-by: multica-agent <github@multica.ai> * docs: include Cursor in skills MCP support Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4594c776e1 |
feat(agent): add CodeBuddy as first-class CLI backend (#3186)
* feat(agent): add codebuddyBackend struct and buildCodebuddyArgs Introduces the codebuddy agent backend skeleton with args builder that mirrors claudeBackend's protocol flags (stream-json, bypass permissions, blocked args filtering) for the codebuddy CLI fork. * feat(agent): implement codebuddyBackend.Execute with stream-json parsing * feat(agent): wire codebuddy into New() factory and launchHeaders * feat(agent): add codebuddy dynamic model discovery from --help * feat(agent): add codebuddy thinking/effort discovery and providerThinkingEnums * feat(daemon): add codebuddy CLI probe, env vars, and args support * fix(agent): use len(models)==0 for default model instead of loop index * fix(agent): increase codebuddy --help timeout to 35s for slow CLI startup * fix(agent): address codebuddy PR review feedback - Wire codebuddy into execenv: reuse claude's CLAUDE.md, .claude/skills, and ~/.claude/skills paths since CodeBuddy is a Claude Code fork - Replace hardcoded 20-min timeout with runContext for zero-timeout = no-deadline semantics matching all other backends - Restore runContext regression tests lost in rebase merge - Mirror claude.go execution model: concurrent stdin write to prevent pipe deadlock, sync.Once for stdin closure, keep stdin open for control_request auto-approval mid-run - Add control_request handling with auto-approve behavior - Add RequestID/Request fields to codebuddySDKMessage - Add codebuddy to metrics knownRuntimeProviders - Add codebuddy to provider-logo.tsx (reuses ClaudeLogo) - Consolidate --help discovery: shared codebuddyHelpOutput cache eliminates duplicate cold-start invocations --------- Co-authored-by: krislliu <krislliu@tencent.com> |
||
|
|
24b162cdbc |
feat(daemon): surface the real task initiator to the agent runtime (MUL-2645) (#3899)
* feat(daemon): surface the real task initiator to the agent runtime (MUL-2645)
In a multi-person workspace the agent runtime only ever saw the runtime
OWNER identity: the brief's `## Requesting User` is sourced from
runtime.OwnerID and the task-scoped token is owner-bound, so every
requester (whoever commented, @mentioned, or chatted) appeared to the
agent as the owner. Agents that route by initiator for permission,
privacy, or audit all misjudged.
Resolve the real task initiator at claim time and surface it distinctly
from the owner:
- comment / mention trigger -> triggering comment's author (member or agent)
- chat task -> chat session creator (sessions are creator-only)
- on-assign / autopilot / quick-create -> no attributable initiator (omitted)
Adds initiator_{type,id,name,email} to the claim response, the daemon
Task, and TaskContextForEnv, rendered into the brief as a new
`## Task Initiator` section. The section documents the privacy boundary:
the agent's credentials stay owner-scoped, so this is an attested
identity for the agent's own routing/privacy logic, not act-as. No DB
migration — both paths are derivable from existing rows.
Tests: brief rendering (member/agent/omit/sanitize) + email guard unit
tests, and claim-handler tests for the comment and chat paths.
Co-authored-by: multica-agent <github@multica.ai>
* fix(chat): store real sender as task initiator, not chat_session creator (MUL-2645)
Review fix (Niko, PR #3899). v1 resolved the chat task initiator from
chat_session.creator_id at claim time. That is correct for web chat and
Lark p2p (creator == sender), but WRONG for Lark group chats: the group
session creator is deliberately the installer (stable identity across
member churn), not the message sender. So in a Lark group, every member
who triggered the agent showed up in the brief as the installer/owner —
the exact bug this issue is about, still live at that entry point.
Capture the real sender at enqueue time instead of deriving it from the
session creator at claim time:
- migration 117: agent_task_queue.initiator_user_id (FK user, ON DELETE
SET NULL); NULL for non-chat and pre-migration rows.
- EnqueueChatTask now takes an explicit initiatorUserID. Web chat passes
the authenticated request user; the Lark dispatcher threads the inbound
sender (binding.MulticaUserID) through scheduleRun -> flushChatRun. The
debouncer keeps the latest scheduled flush per session, so in a multi-
sender silence window the LATEST sender wins (documented + tested).
- claim handler resolves the initiator from task.initiator_user_id and
drops the creator_id fallback entirely.
The Lark group session creator stays the installer (unchanged) — only the
task initiator is corrected, keeping the two concepts cleanly separate.
Tests: dispatcher group regression (initiator = sender, not installer),
latest-sender-wins, p2p initiator assertion; the chat claim handler test
now sets creator != initiator and asserts the stored sender wins.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: J <j@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
5be7d1bc17 |
MUL-3136 fix(openclaw): parse config path from last non-empty line of CLI output
Fix OpenClaw config discovery when `openclaw config file` prints Doctor warning UI before the actual config path. The daemon now uses the last non-empty stdout line as the path while preserving the existing tilde expansion, absolute-path validation, stat checks, and fail-closed behavior. Tests: go test ./internal/daemon/execenv |
||
|
|
4190de3d64 |
fix(skills): quote description values in built-in SKILL.md YAML frontmatter (#3852)
Built-in SKILL.md description values contained unquoted ': ' sequences, which strict YAML parsers (e.g. Codex) reject — silently dropping the skill at load. - Quote all eight built-in skill descriptions. - ensureSkillFrontmatter() re-synthesizes frontmatter that has a name but fails YAML validation, so malformed imports are repaired instead of dropped. - Unify frontmatter delimiter parsing into a single frontmatterParts helper. - Add strict-YAML regression tests over the built-in skills, plus unit tests for the recovery branch and delimiter variants. Closes #3851. |
||
|
|
63b847ee48 |
Honor agent identity in assignment workflow (#3802)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
b9334dd59f |
fix: anchor comment triggers to thread roots (#3746)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d6a556bdbf |
fix(execenv): refresh skills in place on reuse instead of accumulating duplicate dirs (#3716)
Re-dispatching the same agent on the same issue reuses the persistent workdir via execenv.Reuse(), where the standard-provider skill refresh re-wrote skills without clearing the prior dispatch's output, so allocateCollisionFreeSkillDir dodged Multica's own directories into issue-review-multica-N. On reuse, reclaim the platform-owned managed skill directories the prior manifest recorded (removeReusedManagedSkillDirs) and roll back the remaining sidecar files (CleanupSidecars) before refreshing, so each skill lands at its canonical slug every dispatch. Mirrors the Codex hydrateCodexSkills wipe; scoped to reuse, which never runs for local_directory tasks. Fixes #3684 (MUL-2963). |
||
|
|
1544e3b68a |
feat(skills): built-in agent skills (WIP) MUL-2759 (#3456)
* feat(skills): introduce built-in agent skills (WIP) Inject platform-authored, version-bundled skills into every agent on top of its workspace-bound skills, so agents learn how to operate Multica correctly without users needing to know the internals or agents needing to read source. Mechanism: skills are embedded into the server binary and appended to the agent payload at task-claim time (handler/daemon.go), reusing the existing SkillData wire + daemon-side writeSkillFiles. The daemon needs no changes, and because it travels over an existing wire field, older daemons pick the skills up the moment the server ships. First skill: multica-mentioning — how to build a working @mention (look up the UUID, match type to id source, know what each mention type triggers). WIP: injection mechanism + first skill only; more skills to follow in dependency order (skill -> agent -> squad). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(skills): make multica-mentioning the standard template + add eval Add the contract-skill frontmatter the other built-in skills will copy: user-invocable:false (it triggers from context, not as a slash command) and allowed-tools fencing it to the multica CLI it teaches. These keys survive to agent machines untouched (ensureSkillFrontmatter only ever adds a missing name). Add a Go eval in builtin_skills_test.go (a _test.go so it never ships to agent machines via the skill-files walk): - Enforces the template invariants on every built-in skill, present and future: multica- prefix, name+description present, description within 1024 chars, body within the 500-line L2 budget, no eval file leaking into the shipped payload. - Couples the mentioning skill's documented contract to the real util.ParseMentions: its Incorrect examples must parse to nothing (a name where a UUID belongs fails silently) and its Correct example must fire. A drift in the mention regex now breaks CI instead of silently turning the skill into a lie agents act on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(skills): add working-on-issues built-in skill Co-authored-by: multica-agent <github@multica.ai> * feat(skills): verify linked PRs in issue workflow skill Co-authored-by: multica-agent <github@multica.ai> * feat(skills): add skill import and discovery built-ins Co-authored-by: multica-agent <github@multica.ai> * feat(skills): add skill authoring built-in Co-authored-by: multica-agent <github@multica.ai> * docs(skills): align builtin skill workflows Co-authored-by: multica-agent <github@multica.ai> * docs(skills): use structured skill search Co-authored-by: multica-agent <github@multica.ai> * fix(skills): make built-in skill bundle launch-ready Co-authored-by: multica-agent <github@multica.ai> * fix(skills): align built-ins with additive skill binding Co-authored-by: multica-agent <github@multica.ai> * feat(skills): add creating agents built-in skill Co-authored-by: multica-agent <github@multica.ai> * Add built-in squads skill Co-authored-by: multica-agent <github@multica.ai> * refactor(skills): rewrite built-in skills as source-traced contracts Rewrite the built-in agent skills to the inbuilt-skill-authoring standard: state source-traced product facts with the source-code link logic as the core, not prescriptive how-to coaching. - creating-agents: drop the Decision-flow / Do-don't-consequences methodology; replace with field/behavior contracts (validation, persisted shape, daemon claim-time consumption, env gating, skill binding). - skill-discovery: stop teaching repo/github_stars as selection signals — searchClawHubSkills never populates them (always null); rank by install_count + source/url + description. Add file:line citations. - mentioning: drop the unbacked "member mention sends a notification" claim (no such path in the comment handler); state that only agent/squad mentions enqueue work. Tighten the parser-failure wording. - working-on-issues: refresh citations drifted by the main merge; describe the PR response `state` enum accurately; trim status coaching. - skill-importing: correct response type to SkillWithFilesResponse; document the reserved SKILL.md supporting-file rule; add line-accurate citations. - squads: correct the "leader cannot be archived" overstatement (not rejected at create/update; fails closed later at routing/dispatch); refresh source-map attributions and test list. Each skill now ships references/<skill>-source-map.md as its evidence layer (line-accurate citations live there, not pinned in the test, so a future main merge cannot rot them into stale lies). builtin_skills_test.go: replace coaching/line-number pins with drift-resistant contract anchors, forbid the coaching phrasing, and require every skill to ship its source-map. The ParseMentions behavior coupling is preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * docs(skills): close field-role and citation gaps found in review Independent review of the rewritten built-in skills surfaced two real gaps and some citation drift; this fixes them. - creating-agents: add the three missing field rows (visibility, max_concurrent_tasks, mcp_config) to the field-contract table — mcp_config is runtime-consumed (TaskAgentData, daemon.go), visibility is access-control (default private), max_concurrent_tasks is a scheduler cap (default 6). Mark custom_args/runtime_config JSON validation as CLI-side (the server marshals as-is). Correct the CLI body-builder note (description/instructions use a non-empty check, the rest use Changed). Source-map: fix the env query name (UpdateAgentCustomEnv), the conformance test name, and add the new field defaults + the McpConfig runtime-payload line. - mentioning: the @squad mention private gate is canAccessPrivateAgent, not canEnqueueSquadLeader (that wrapper is the assignment/child-done path). - working-on-issues: cite notifyParentOfChildDone at its func def (:51), not the doc comment (:15). - skill-importing: config.origin is set only when the source supplied an origin — note it may be absent; cite createSkillWithFiles at its definition (skill_create.go:72), not the call site. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * Add built-in skills for autopilots runtimes and resources Co-authored-by: multica-agent <github@multica.ai> * feat(runtime): list skill descriptions in the brief Skills index The brief's `## Skills` section emitted bare skill names only, discarding the one-line description that SkillContextForEnv already carries. For Claude-family providers the frontmatter description is loaded natively; for providers without native skill discovery (hermes/default) the brief's list is the only signal they ever see, so a bare name gave them nothing to decide when to load a skill. Emit `name — description` when a description is present, falling back to the bare name when it is empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(skills): drop CLI-only rule from working-on-issues The "Platform data goes through the CLI" section duplicated the runtime brief's `## Important: Always Use the multica CLI` section verbatim (and the attachment-via-CLI note duplicated the brief's `## Attachments`). The CLI-only rule is universal and must be known before any skill loads, so the brief is its single source of truth; the skill copy was pure redundancy and a drift risk. Remove it and the matching intro clause. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * refactor(skills): remove discovery guidance from built-ins * docs(skills): remove stale skill-necessity records The per-skill necessity records had drifted to 3 of 8 shipped skills plus a record for `multica-skill-authoring`, which is not a shipped built-in skill. Per-skill "why it exists / when to use it" already lives co-located with each skill (frontmatter `description` + `references/<skill>-source-map.md`) where it cannot drift from the skill, and the doc's methodology duplicated the workspace's inbuilt-skill-authoring protocol. Remove the file rather than keep a parallel listing that every new skill has to remember to update. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * feat(runtime): add source-authority escape hatch to the brief The brief already tells agents to run `--help` for command discovery, but nothing stated the trust precedence when a skill, the brief, or a doc seems to contradict actual behavior. Add one line to the Available Commands escape-hatch note: trust the live CLI (`--help`/`--output json`) and the checked-out source over source-traced prose that can lag the code, and verify on any conflict or confusion. Kept in the always-on brief (universal, needed before any skill loads) rather than duplicated into each skill; per-skill source-map pointers remain the specific layer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(runtime): scope the source-authority escape hatch to the CLI The previous version told agents the "checked-out source is the deeper authority" for verifying behavior. That over-claims: the repos in a task's brief come from GetWorkspaceRepos + project github_repo resources (per-workspace config, see daemon.registerTaskRepos), not the Multica platform source. A generic agent's checked-out source is its own app, not Multica's code, so it cannot verify a Multica skill/brief claim against it. The only universally available authority for Multica behavior is the live CLI (`--help` / `--output json` / observed command behavior). Re-scope the line accordingly and state plainly that the platform's source is not in the workdir. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * revert(runtime): drop the source-authority escape-hatch line Reverts the brief addition from |
||
|
|
44feb3d06d |
fix(skill): canonicalize reserved SKILL.md path check across daemon + API (#3660)
A skill_file row whose path is the skill's own SKILL.md (persisted by older builds or direct create/update API calls) collides with the primary content the daemon writes itself, failing task prep with errPathPreExists on every non-codex local runtime (#3489). #3526 guarded this with strings.EqualFold(path, "SKILL.md") at the daemon write site and the three API ingress points, but the stored path is not canonicalized: "./SKILL.md" or "sub/../SKILL.md" slip past the exact-match guard while filepath.Join still resolves them onto the same SKILL.md, so prep can still break. Extract one canonical helper, skill.IsReservedContentPath, that cleans the path before the case-insensitive compare, and use it at all four sites (execenv writeSkillFiles, skill create, update, single-file upsert). Add a daemon-side regression test for writeSkillFiles ignoring a bundled SKILL.md (exact + "./" spellings) — the load-bearing fix previously had only API-layer coverage — plus a unit test for the helper. Existing poisoned rows are intentionally left in place (skipped at prep) per the decision on MUL-2928. MUL-2928 Follow-up to #3526; supersedes #3560. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
996eb07dc5 |
fix(daemon): skip duplicate SKILL.md in supporting files to prevent task prep failures (#3526)
Fixes #3489 MUL-2928 |
||
|
|
888186b183 |
fix(daemon): make comment-posting guardrail provider-agnostic (MUL-2904) (#3654)
* fix(daemon): make comment-posting guardrail provider-agnostic (MUL-2904) Agents inlining a backtick-wrapped token into `multica issue comment add --content "..."` had the shell run it as a command substitution, silently deleting the token; the stored comment never matched the model's intent, so it retried forever — spamming OKK-497 with duplicate comments. The corruption is shell-driven, not provider-driven, so extend the "never inline --content; use --content-file / quoted-HEREDOC --content-stdin" rule from Codex-only to ALL providers: - BuildCommentReplyInstructions: collapse the Linux/macOS non-Codex inline branch into the unified quoted-HEREDOC stdin template. - buildMetaSkillContent: rename "Codex-Specific Comment Formatting" -> "Comment Formatting" and emit it for every provider; strengthen the Available Commands entry and the assignment step-6 examples to steer away from inline --content. - Windows behavior unchanged (file-only; avoids PowerShell ASCII drop). Tests: flip the non-Codex Linux reply test into a MUL-2904 regression, broaden the stdin-emphasis test across providers, and pin the provider-agnostic guardrail. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): keep Windows assignment brief file-only (address review) Review catch on #3654: the previous commit added platform-agnostic prose recommending "--content-file or --content-stdin" in the Available Commands entry and the assignment-triggered step-6 example. The assignment path has no BuildCommentReplyInstructions OS override, so on Windows an agent following step 6 literally would pipe its final comment through PowerShell and drop non-ASCII bytes (#2198 / #2236 / #2376) — contradicting this PR's own Windows file-only rule in the ## Comment Formatting section. Make the platform-agnostic surfaces defer to the OS-aware ## Comment Formatting section (the single source of truth) instead of naming stdin. The flag synopsis still lists all three modes. Add TestInjectRuntimeConfigWindowsAssignmentBriefStaysFileOnly: a Windows assignment-triggered brief must not contain any prescriptive "... or --content-stdin" recommendation. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3c8645e546 |
feat(cli): add squad member set-role (#3583)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
4ae4722ef0 |
fix(comments): preserve direct parent on replies (#3579)
Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
973a43923f |
fix(comments): revert since-delta to issue-wide, steer to parent thread first (#3535)
#3509/#3523 scoped the comment-trigger since-delta count to the triggering
thread, so an agent resuming a busy issue only saw "+N in this thread" and
lost visibility of new comments in other threads. Revert the count to
issue-wide (every thread), keeping the trigger-comment + agent-own
exclusions, and reshape the warm-path hint to:
- report the issue-wide new-comment volume,
- steer the agent to read the triggering (parent) thread FIRST
(`--thread <trigger> --since`, or `--tail 30` for full context),
- demote the issue-wide `--since` catch-up to an only-if-needed fallback
("don't read them all blindly").
Also fixes the now-stale "scoped to the triggering thread" wording in the
resumed-session no-delta hint (it's issue-wide zero now).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
d1c7d478e1 |
MUL-2785: clarify thread-scoped comment delta (#3523)
Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9616d78e47 |
MUL-2785: optimize resumed comment reads (#3509)
* feat(comments): skip default thread read on resumed comment sessions Co-authored-by: multica-agent <github@multica.ai> * fix(comments): scope since delta to trigger thread Co-authored-by: multica-agent <github@multica.ai> * chore(comments): address thread delta review nits Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
3187bbf90c |
feat(comments): re-add since-delta + cold-start thread read + parent-root write normalization (#3494)
* feat(comments): since-delta new-comment hint + default-on comment session resume (#3432) * feat(db): add unresolved comment count + list filter queries Add CountUnresolvedComments (excludes the agent's own comments) and ListUnresolvedCommentsForIssue. Both are additive — existing callers stay on the unfiltered queries — so old clients are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): support unresolved-only comment listing Wire an additive `unresolved` query param into ListComments. Defaults off so an old CLI that never sends it gets unchanged behavior; only true/1 enable it. Rejects combining unresolved with thread/recent (whole-issue filter vs navigation models). Includes filter + count query tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): plumb unresolved count + thread root into claim, gate comment resume Populate trigger_parent_id (thread root of the trigger comment) and unresolved_count (excludes the agent's own comments) on comment-triggered claim responses. Both fields are omitempty so old daemons ignore them. Gate comment-triggered session resume behind MULTICA_RESUME_COMMENT_SESSION (default off): resumed comment turns can inherit the prior turn's "Done." final message, so this stays an explicit rollout switch. The runtime-match and poisoned-session guards still apply regardless of the flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(daemon): inject unresolved-comments hint + resolve step into agent brief Add a shared BuildUnresolvedCommentsHint helper rendered on both the per-turn prompt and the CLAUDE.md workflow (kept in sync per PR #2816). It ships only the count and the relevant CLI call — never comment bodies — so the server stays cheap. Thread case points at --thread <root>; issue case points at --unresolved. Suppressed when the count is 0. Also add a workflow step telling the agent to `multica comment resolve <thread-root>` once a thread is fully handled, so the unresolved set converges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): add comment list --unresolved and comment resolve command Add an --unresolved filter to `issue comment list` (wired to the server's unresolved param, rejected when combined with --thread/--recent) and a top-level `comment resolve <id>` command that POSTs to the existing /api/comments/{id}/resolve endpoint, letting an agent close threads it has fully handled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(comments): since-delta new-comment hint + default-on comment resume Simplifies the comment-triggered agent flow down to what's actually needed: - New-comment awareness is now a pure time delta: the claim response carries new_comment_count + new_comments_since (anchored on the prior run's started_at, never completed_at so a long run can't miss comments). The per-turn prompt and CLAUDE.md workflow render one line — "N new comment(s) since your last run, --since <ts>" — via a shared BuildNewCommentsHint so the two surfaces can't drift. Cold start (no prior run) falls back to a plain read. - Comment-triggered tasks resume the prior session by default (same runtime), dropping the MULTICA_RESUME_COMMENT_SESSION rollout gate. The "Focus on THIS comment" prompt guard defends against inheriting the prior turn's "Done." marker; GetLastTaskSession still excludes poisoned sessions. - Drops the resolved-based machinery from the first draft: CountUnresolvedComments / ListUnresolvedCommentsForIssue queries, the `comment list --unresolved` flag, the `multica comment resolve` command, and the resolve workflow step. - Removes the verbose cursor-pagination paragraph from the comment prompt; the --thread/--recent/--since flags stay in the CLI/API, just no longer explained inline every turn. Compatibility: new claim fields are omitempty (old daemons ignore them). Comment resume is default-on and affects even old daemons, which already consume prior_session_id. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(comments): collapse reply parent_id to thread root on write Comment threads are a 2-level model (root + flat replies, like Linear/Slack), enforced today only by the UI and the agent path — the CreateComment handler stored whatever parent_id it was handed, and the agent-side flatten walked just one level, so a reply-to-a-reply could land at depth 3+. Add GetThreadRoot (a recursive walk to the parent_id=NULL root) and run both write paths (handler.CreateComment, service.createAgentComment) through it, so every stored reply's parent_id IS its thread root. Readers can now treat parent_id as the thread root without re-walking. The agent-drift guard still compares the raw parent_id to the trigger comment before normalization. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(comments): cold-start reads triggering thread, warm keeps --thread pointer The since-delta rework dropped the thread-first read on the COLD path: a first-time agent fell back to the flat `comment list` dump (oldest-first, cap 2000), burying the trigger's context in ancient chatter. Point cold start at the triggering conversation instead via a shared BuildColdCommentsHint (`--thread <trigger> --tail 30` + a --recent pointer for cross-thread background). On the WARM path, --since is a pure time delta and can miss the triggering thread's pre-anchor history, so BuildNewCommentsHint now also emits a --thread pointer. Both surfaces (per-turn prompt + CLAUDE.md workflow) render via the shared helpers so they cannot drift (PR #2816 rule). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fa076d38f2 |
MUL-2778 feat(agent): wire mcp_config through OpenClaw runtime (#3450)
* MUL-2778 feat(agent): wire mcp_config through OpenClaw runtime The MCP config tab (#3419) lets admins save mcp_config on an agent, and recent work (#3439) plumbed it through the three ACP runtimes. OpenClaw still ignored the field, leaving the Tab silently inert for any OpenClaw-backed agent. Translate the agent's Claude-style `{"mcpServers": {...}}` into the per-task OpenClaw wrapper's `mcp.servers` block — OpenClaw resolves MCP via its own config schema rather than ExecOptions, so the existing OPENCLAW_CONFIG_PATH preparer is the right seam. Fail closed on malformed JSON / entries missing `command` or `url`, matching the fail-closed posture the preparer already uses for the agents.list step. Null / absent mcp_config leaves the wrapper free of an `mcp` key so the user's global mcp.servers flows through untouched; an explicit empty managed set (`{}` / `{"mcpServers":{}}`) is honoured as "admin saved no servers" mirroring `hasManagedCodexMcpConfig`. Strict-mode replacement (drop user-only servers entirely) would require OpenClaw to do a per-key replace rather than a deep merge at `mcp.servers`; the comment documents that caveat rather than relying on undocumented behaviour. Also adds `openclaw` to `MCP_SUPPORTED_PROVIDERS` so the MCP Tab actually surfaces in the agent overview pane, and pins the new visibility case with a renderPane test. Co-authored-by: multica-agent <github@multica.ai> * MUL-2778 fix(agent): make openclaw mcp_config strict-replace via sanitized snapshot Elon flagged on #3450 that the previous wiring let user-only mcp.servers leak through the wrapper's `$include` of the live user config: deep-merge at `mcp.servers` keeps user-only names, and the strict-empty case (`{ "mcpServers": {} }`) silently inherited user globals. Switch the strict-replace path to write a sanitized snapshot of the user's fully resolved config (via `openclaw config get --json`) with the `mcp` block stripped, then have the wrapper `$include` the snapshot instead of the live user file. With the user's `mcp` gone from the $include resolution, the wrapper's `mcp.servers` is the only definition the embedded OpenClaw sees — managed only, including the explicit empty set. The snapshot lives in envRoot at 0o600 alongside the wrapper so the GC reaper sweeps it with the rest of the task scratch, and no extra OPENCLAW_INCLUDE_ROOTS entry is needed (same-dir $include). Fail-closed on `config get --json` errors so the daemon never silently falls back to the leaky $include path. The inherit branch (null mcp_config) still uses the live user file directly — no extra CLI roundtrip and no snapshot is written. New tests pin the contract Elon's review required: - TestPrepareOpenclawConfigStrictReplacesUserMcpServers: user has global_one + shared, managed has shared + managed_only → wrapper has exactly {shared (managed value), managed_only}; global_one does NOT leak; snapshot file has the user's `mcp` stripped while preserving gateway / providers / API keys. - TestPrepareOpenclawConfigStrictEmptyManagedSetDropsUserMcp: empty managed set drops user's global_one (both `{}` and `{"mcpServers":{}}` cases). - TestPrepareOpenclawConfigNullMcpConfigKeepsUserInclude: null path inherits the live user config, writes no snapshot, makes no extra CLI call. - TestPrepareOpenclawConfigFailsClosedOnResolvedConfigError: errors during `config get --json` surface; no stale wrapper or snapshot. - TestPrepareOpenclawConfigManagedSetFreshInstall: fresh install with managed mcp_config skips the snapshot dance entirely. Also tightens en + zh-Hans MCP Tab copy to mention OpenClaw goes via the per-task wrapper, and to use OpenClaw's own `transport` field rather than Claude's `type` for HTTP/SSE entries. Co-authored-by: multica-agent <github@multica.ai> * MUL-2778 fix(agent): narrow openclaw snapshot strip to mcp.servers only Elon's third-round must-fix: the previous strict-replace snapshot deleted the entire `mcp` block, which wiped out non-server settings under `mcp` like `sessionIdleTtlMs`. Those are documented OpenClaw config keys (https://docs.openclaw.ai/gateway/configuration-reference#mcp) outside the MCP Tab's scope — the agent's saved mcp_config only manages server definitions, so other `mcp.*` tuning the user set must survive. Replace the blanket `delete(resolved, "mcp")` with a stripUserMcpServers helper that: - deletes only `mcp.servers` when `mcp` is an object - drops the parent `mcp` key only when the object is empty after the strip (so we don't emit `mcp: {}` placeholders) - leaves non-object `mcp` values untouched (we only know how to strip servers from the documented shape) Pinned with TestPrepareOpenclawConfigStrictPreservesNonServerMcpKeys: user resolved has both `mcp.sessionIdleTtlMs: 300000` and `mcp.servers.global_one`; after the strict path runs the snapshot keeps the TTL and drops the servers map, and the wrapper's `mcp.servers` is exactly the managed set with no leak. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
d90732750f |
Revert "feat(comments): since-delta new-comment hint + default-on comment ses…" (#3455)
This reverts commit
|
||
|
|
ee4ec3b76d |
MUL-2784 fix(daemon): cleanup sidecar tree (.agent_context / .multica / provider skills) after local_directory tasks (#3444)
* fix(daemon): cleanup .agent_context / .multica / provider skill sidecars after local_directory tasks (MUL-2784) PR #3438 (MUL-2753) only restored CLAUDE.md / AGENTS.md / GEMINI.md to their pre-task bytes; the sidecar tree writeContextFiles seeds (.agent_context/, .multica/, .claude/skills/, .github/skills/, .opencode/skills/, skills/, .pi/skills/, .cursor/skills/, .kimi/skills/, .kiro/skills/, .agents/skills/, fallback .agent_context/skills/) was explicitly deferred to this follow-up. In local_directory mode the agent's workdir is the user's repo, so each task accumulates one more layer of those directories in the user's tree. Plan A: track every file/dir Prepare creates inside workDir in a sidecarManifest written to envRoot/.multica_sidecar_manifest.json (daemon scratch — never in the user's workdir). On local_directory teardown CleanupSidecars walks the manifest, removes the recorded files, then rmdir-iterates the recorded directories in reverse. Pre-existing files and directories are deliberately NOT recorded, so a user-installed .claude/skills/my-own-skill/ sibling — or any unrelated file the user keeps under .claude/, .github/, etc. — is preserved bit-for-bit. Non-empty rmdir fails ENOTEMPTY and is silently skipped, which is the signal that the user owns the directory. Daemon wiring lives next to the existing CleanupRuntimeConfig defer in runTask: runtime brief first, sidecars second. Cloud-mode runs still write a manifest for symmetry but never trigger the cleanup (the GC loop wipes envRoot wholesale). Tests (sidecar_manifest_test.go) cover the round-trip invariant per the issue's acceptance criteria: - empty workdir → Prepare → Cleanup → empty workdir, byte-exact, for every file-based provider (claude, codex, copilot, opencode, openclaw, hermes, pi, cursor, kimi, kiro, antigravity, gemini), - user's .claude/skills/my-own-skill/ (and equivalents per provider) survives Cleanup intact, - unrelated user files under .claude/, .github/, etc. survive, - three repeated cycles do not accumulate any orphan state, - project_resources branch (.multica/project/resources.json) is also reversible, - recordWriteFile refuses to record pre-existing files, - recordMkdirAll refuses to record pre-existing dirs, - Cleanup is a no-op when the manifest file is missing. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): refuse to overwrite pre-existing sidecar paths; pick collision-free skill slugs (MUL-2784 review) Addresses PR #3444 review (Elon): **Must-fix #1**: recordWriteFile used to overwrite pre-existing target files unconditionally and only skip the manifest record. That destroys user bytes at write time AND leaves the corrupted contents in place at cleanup time — the byte-exact contract the issue requires is violated on both halves. Fixed by making recordWriteFile detect any pre-existing entry (regular file, symlink, directory) via Lstat and return a sentinel errPathPreExists without touching the path. The user's bytes are preserved verbatim. For per-skill collisions (user's .claude/skills/issue-review/ vs Multica's "Issue Review"), writeSkillFiles now allocates a collision-free sibling slug via allocateCollisionFreeSkillDir: first attempt is the natural slug, then `<base>-multica`, `<base>-multica-2`, …, bounded at 64 attempts. Provider-native discovery still picks the skill up (every subdir under skillsParent is a distinct skill) and the user's path stays bit-for-bit intact. For Multica-only namespace files (.agent_context/issue_context.md, .multica/project/resources.json), the writer swallows errPathPreExists and continues — the runtime brief already carries every fact those files would, so a collision degrades to brief-only mode rather than destroying user content. **Must-fix #2**: Added byte-exact collision matrix tests covering every file-based provider (claude / codex / copilot / opencode / openclaw / hermes / pi / cursor / kimi / kiro / antigravity / gemini): - TestPrepareThenCleanupSidecarsSameSlugCollisionPerProvider: seeds user's `<provider>/skills/issue-review/SKILL.md` plus a private notes.md sibling, runs Prepare → Inject → Cleanup, asserts workdir snapshot is byte-identical to seed. - TestPrepareThenCleanupSidecarsIssueContextCollisionPerProvider: seeds user's `.agent_context/issue_context.md`, asserts round-trip preserves it. - TestPrepareThenCleanupSidecarsProjectResourcesCollisionPerProvider: same for `.multica/project/resources.json`. - TestPrepareThenCleanupSidecarsMultiSkillCollisionFreeAllocation: end-to-end check that the Multica skill lands at the collision-free sibling and Cleanup removes only the Multica side. - TestAllocateCollisionFreeSkillDir: directed unit test pinning the slug-bumping sequence. - TestRecordWriteFileRefusesToOverwritePreExistingFile (was TestRecordWriteFileSkipsPreExistingFile): flipped to assert the user's bytes survive and errPathPreExists is returned. - TestRecordWriteFileRefusesToOverwriteSymlinkOrDir: covers the Lstat path for non-file entries. **Should-fix**: CleanupSidecars used to swallow ANY non-ENOENT rmdir error as "user content present," silently dropping real I/O failures (EACCES, EPERM, EBUSY). Now it re-reads the directory after a failed rmdir via the new dirHasEntries helper — non-empty → silently skip (ENOTEMPTY, the intended branch); empty → genuine error, captured into firstErr and surfaced. Plus directed tests: - TestCleanupSidecarsSurfacesRealRmdirErrors - TestDirHasEntries Local verification: - go test ./internal/daemon/execenv/... — all green - go test ./internal/daemon/... — all green - go vet ./... — clean Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): surface original rmdir error when post-rmdir ReadDir also fails (MUL-2784 review) Addresses remaining PR #3444 review blocker (Elon): dirHasEntries used to return true when ReadDir failed with anything other than ENOENT, which made CleanupSidecars treat every locked / faulted directory as ENOTEMPTY and silently drop the original rmdir error. The v1 fix from the previous round closed the EACCES-on-empty-dir branch but missed the case where the chmod also blocks ReadDir — exactly the failure mode the review called out. Helper change: dirHasEntries now returns (hasEntries, ok bool): - (false, true) — dir exists and is empty (or missing, race-safe) - (true, true) — dir has user content (the ENOTEMPTY branch) - (_, false) — ReadDir failed (EACCES, ENOTDIR, EIO, …); the caller cannot tell ENOTEMPTY from a real error and MUST surface the original rmdir error CleanupSidecars switches on (ok, hasEntries): - !ok → surface the ORIGINAL rmdir error (not the ReadDir failure — that's diagnostic plumbing and would distract from the root cause) - ok && hasEntries → swallow silently (intended ENOTEMPTY branch; preserve user content) - ok && !hasEntries → surface the rmdir error (empty dir + EACCES / EPERM / EBUSY → genuine cleanup failure) Tests: - TestDirHasEntries: extended with a regular-file sub-case (ReadDir returns ENOTDIR) asserting (false, false). The v1 helper returned (true) here, hiding the bug. - TestCleanupSidecarsSwallowsMissingAndNonEmptyDirs: renamed from TestCleanupSidecarsSurfacesRealRmdirErrors. The old name claimed to test the surfacing path but never actually exercised it. - TestCleanupSidecarsSurfacesEACCESOnEmptyRecordedDir: chmod parent to 0o555 so rmdir(recorded) fails EACCES while ReadDir(recorded) still succeeds (empty). Asserts firstErr is non-nil and references both the recorded path and the rmdir branch. Skipped when running as root (chmod is bypassed for uid 0). - TestCleanupSidecarsSurfacesEACCESWhenReadDirFailsToo: the must-fix case — chmod parent 0o555 AND chmod recorded 0o000 so BOTH rmdir and ReadDir fail. The surfaced error must be the ORIGINAL rmdir failure, not the ReadDir one. Skipped on uid 0. Local verification: - go test ./internal/daemon/execenv/... — all green - go test ./internal/daemon/... — all green - go vet ./... — clean Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
03f70209c4 |
fix(daemon): preserve user CLAUDE.md / AGENTS.md / GEMINI.md in local_directory runs (#3438)
* fix(daemon): preserve user CLAUDE.md / AGENTS.md / GEMINI.md in local_directory runs (MUL-2753) InjectRuntimeConfig previously called os.WriteFile unconditionally, which truncated whatever file lived at the same path. For the local_directory project_resource flow the workdir is the user's own repo, so the agent silently destroyed any repo-level CLAUDE.md / AGENTS.md / GEMINI.md the first time it ran in that directory, and the daemon's local-directory cleanup explicitly skips the user's path so the file was never restored. Write the brief inside a marker block instead: <!-- BEGIN MULTICA-RUNTIME (auto-managed; do not edit) --> ...brief... <!-- END MULTICA-RUNTIME --> writeRuntimeConfigFile handles three states: - file missing -> create with just the marker block, - file present, no marker block -> append the marker block at the end (preserves user-authored content above), and - file present, marker block already there -> replace the block body in place so repeated runs don't grow the file unboundedly. This is the short-term fix called out on MUL-2753. The sidecar question (.agent_context/, .claude/skills/, .multica/project/resources.json) is left for a follow-up — those files don't overwrite user content, just litter the workdir. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): cleanup runtime config marker block after local_directory tasks (MUL-2753) Address Elon's review on PR #3438: 1. Add `CleanupRuntimeConfig` and wire it into the daemon's task path so `local_directory` runs excise the marker block on the way out. Without it, a user's subsequent manual `claude` / `codex` / `gemini` run in the same directory picks up the previous task's stale brief (issue id, trigger comment id, reply rules) and acts on the wrong context. Cloud workspace runs skip the cleanup — their scratch workdir is wiped by the GC loop anyway. 2. If excising the block would leave the file empty / whitespace-only, the file is removed so we don't leave behind a stub the user has to delete by hand. Surviving user content is preserved byte-for-byte. 3. Harden the marker parser: search for the end marker strictly after the begin marker. The previous `strings.Index` pair mishandled two malformed cases — - a stray end marker before any begin (e.g. user pasted a documentation snippet showing the wire format) would cause every run to stack another block, growing the file unboundedly; - a half-block left by a previous crashed run would cause every subsequent run to append a fresh block beneath the half-block. The `locateMarkerBlock` helper now anchors the end search past the begin offset, and treats "begin found, no end after" as "block runs to EOF" so the next write replaces it cleanly. Centralised the provider→filename mapping in `runtimeConfigPath` so Inject and Cleanup can't drift past each other when a new provider is added. Tests cover: parser hardening (stray-end-before-begin idempotency, half-block recovery), Cleanup happy path / file removal / no-op cases / malformed half-block / per-provider mapping, and an end-to-end inject→cleanup round trip that locks in byte-identical restoration of the user's pre-injection file. Co-authored-by: multica-agent <github@multica.ai> * fix(daemon): byte-exact inject/cleanup round trip for runtime config (MUL-2753) Address Elon's second-round review on PR #3438. The previous cleanup relied on `TrimRight + "\n"` for trailing newlines and `TrimSpace == ""` for file removal — both compensated for the inject path's "normalise trailing newlines so there's always exactly `\n\n` before the block" step, but they did so by mutating the user's bytes. The result was a real diff on three boundary cases: - file ended without a newline (`rules`) → cleanup added one; - file ended with two or more newlines (`rules\n\n`) → cleanup collapsed to a single newline; - file pre-existed but was empty / whitespace-only → cleanup deleted it. Reshape the contract so the bytes inject adds are the exact bytes cleanup removes, with no user-byte mutation in between: - Define `runtimeManagedSeparator = "\n\n"` as a fixed managed separator that inject always inserts (unconditionally — including for files that already end in two or more newlines) between pre-existing user content and the marker block. - Inject's missing-file branch still writes the block alone (no separator); that absence is the marker Cleanup uses to identify "we created this file from scratch" and is the only condition under which Cleanup is allowed to `os.Remove` the file. - Cleanup detects `HasSuffix(pre, runtimeManagedSeparator)` and strips exactly those bytes; whatever remains is written back verbatim with no `TrimRight` / `TrimSpace`, so the pre-injection bytes survive exactly. The replace-in-place branch is untouched — the managed separator established by the first inject lives in pre and survives across subsequent runs, so byte-exactness is preserved through arbitrary inject→inject→cleanup chains. Tests: - `TestInjectThenCleanupRoundTripByteExactBoundaries` parameterises 9 seed shapes (missing file, empty, whitespace-only, no trailing newline, one trailing newline, two trailing newlines, many trailing newlines, CRLF line endings, no final newline with embedded blank lines) and asserts byte-identical round trip across two full cycles. - `TestInjectReplaceThenCleanupRestoresByteExact` covers the replace-in-place branch for the same boundary seeds. - `TestWriteRuntimeConfigFileAlwaysInsertsFixedManagedSeparator` pins the new invariant at the source: regardless of seed shape, inject emits `<seed><\n\n><marker block>` with no normalisation. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
5e78e5100a |
feat(comments): since-delta new-comment hint + default-on comment session resume (#3432)
* feat(db): add unresolved comment count + list filter queries Add CountUnresolvedComments (excludes the agent's own comments) and ListUnresolvedCommentsForIssue. Both are additive — existing callers stay on the unfiltered queries — so old clients are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): support unresolved-only comment listing Wire an additive `unresolved` query param into ListComments. Defaults off so an old CLI that never sends it gets unchanged behavior; only true/1 enable it. Rejects combining unresolved with thread/recent (whole-issue filter vs navigation models). Includes filter + count query tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(handler): plumb unresolved count + thread root into claim, gate comment resume Populate trigger_parent_id (thread root of the trigger comment) and unresolved_count (excludes the agent's own comments) on comment-triggered claim responses. Both fields are omitempty so old daemons ignore them. Gate comment-triggered session resume behind MULTICA_RESUME_COMMENT_SESSION (default off): resumed comment turns can inherit the prior turn's "Done." final message, so this stays an explicit rollout switch. The runtime-match and poisoned-session guards still apply regardless of the flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(daemon): inject unresolved-comments hint + resolve step into agent brief Add a shared BuildUnresolvedCommentsHint helper rendered on both the per-turn prompt and the CLAUDE.md workflow (kept in sync per PR #2816). It ships only the count and the relevant CLI call — never comment bodies — so the server stays cheap. Thread case points at --thread <root>; issue case points at --unresolved. Suppressed when the count is 0. Also add a workflow step telling the agent to `multica comment resolve <thread-root>` once a thread is fully handled, so the unresolved set converges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): add comment list --unresolved and comment resolve command Add an --unresolved filter to `issue comment list` (wired to the server's unresolved param, rejected when combined with --thread/--recent) and a top-level `comment resolve <id>` command that POSTs to the existing /api/comments/{id}/resolve endpoint, letting an agent close threads it has fully handled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(comments): since-delta new-comment hint + default-on comment resume Simplifies the comment-triggered agent flow down to what's actually needed: - New-comment awareness is now a pure time delta: the claim response carries new_comment_count + new_comments_since (anchored on the prior run's started_at, never completed_at so a long run can't miss comments). The per-turn prompt and CLAUDE.md workflow render one line — "N new comment(s) since your last run, --since <ts>" — via a shared BuildNewCommentsHint so the two surfaces can't drift. Cold start (no prior run) falls back to a plain read. - Comment-triggered tasks resume the prior session by default (same runtime), dropping the MULTICA_RESUME_COMMENT_SESSION rollout gate. The "Focus on THIS comment" prompt guard defends against inheriting the prior turn's "Done." marker; GetLastTaskSession still excludes poisoned sessions. - Drops the resolved-based machinery from the first draft: CountUnresolvedComments / ListUnresolvedCommentsForIssue queries, the `comment list --unresolved` flag, the `multica comment resolve` command, and the resolve workflow step. - Removes the verbose cursor-pagination paragraph from the comment prompt; the --thread/--recent/--since flags stay in the CLI/API, just no longer explained inline every turn. Compatibility: new claim fields are omitempty (old daemons ignore them). Comment resume is default-on and affects even old daemons, which already consume prior_session_id. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bae8a84abd |
MUL-2767 feat(agent): add Antigravity runtime backend (#3427)
* feat(agent): add Antigravity runtime backend Adds Google's Antigravity CLI (`agy`) as the 12th supported coding-tool runtime, alongside Claude / Codex / Cursor / Copilot / Gemini / Hermes / Kimi / Kiro / OpenCode / OpenClaw / Pi. The CLI emits plain assistant text on stdout (no structured event stream), so the backend streams stdout line-by-line as `MessageText` events and accumulates the same text as the final `Result.Output`. Session resumption uses `--conversation <id>`; because the conversation UUID is not echoed on stdout, the daemon routes `--log-file` to a temp file and recovers the id from the glog-formatted log lines. MUL-2767 Co-authored-by: multica-agent <github@multica.ai> * fix(agent): correct Antigravity capability contract from Elon review - ModelSelectionSupported now returns false for antigravity. `agy` has no --model flag and antigravityBackend deliberately drops opts.Model, so the UI must render a disabled "Managed by runtime" picker instead of an empty dropdown plus a silently-ignored manual-entry field. Also stop seeding AgentEntry.Model from MULTICA_ANTIGRAVITY_MODEL — the backend would silently ignore it. - Antigravity skills now write to {workDir}/.agents/skills/, the CLI's native workspace path (inherits Gemini CLI's layout per https://antigravity.google/docs/gcli-migration). Previously they went to the .agent_context/skills/ fallback that the CLI doesn't scan. Runtime brief moves antigravity into the native-discovery branch and local_skills.go points the user-level skill root at ~/.gemini/antigravity-cli/skills for Runtime → local skill import. - Doc + UI comment sync: providers matrix / install-agent-runtime / cloud-quickstart / agents-create / tasks (session-resume support) / skills / README all now list Antigravity in the right buckets, and the model-picker / model-dropdown comments cite antigravity (not the stale hermes reference) as the supported=false example. New tests: TestAntigravityModelSelectionUnsupported, TestInjectRuntimeConfigAntigravity (native discovery wording), TestWriteContextFilesAntigravityNativeSkills (.agents/skills/ landing, .agent_context/skills/ NOT written). Co-authored-by: multica-agent <github@multica.ai> * feat(provider-logo): swap inline placeholder for real Antigravity PNG Replaces the hand-drawn planet+arc placeholder with the official asset shipped from Downloads. Stored next to the component; bundlers (Next.js / electron-vite) resolve the PNG import to a URL string at build time. Added a small assets.d.ts so packages/views' tsc accepts PNG / SVG module imports — there was no prior asset usage in this package to register the declaration. --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
341ce7bfa5 |
feat: support local working directory for projects (MUL-2618 v1) (#3283)
* feat(project): add local_directory project_resource type (MUL-2662)
Adds a second project_resource type alongside github_repo so a project
can be pinned to an existing directory on a specific daemon (the v1 of
the local-working-directory flow tracked in MUL-2618). The ref schema is
{ local_path, daemon_id, label? }; local_path must be absolute and
daemon_id is required. The same (daemon_id, local_path) pair is allowed
on multiple projects by design — no UNIQUE constraint is added.
Implementation reuses the existing project_resource API surface: the new
type is wired through the validator switch with no migration, no new
events, and no daemon-handler changes (daemon already passes through
arbitrary resource types via ProjectResources). The CLI gains
--local-path / --daemon-id / --ref-label shortcuts so
`multica project resource add --type local_directory` mirrors the
existing `--type github_repo --url ...` ergonomics; the generic --ref
flag still works for both types.
Tests cover the full CRUD lifecycle, the same-path-across-projects
allowance, the same-path-same-project conflict, the validator rejections
(missing/blank/relative path, missing daemon_id, wrong payload type),
and the cross-platform isAbsoluteLocalPath helper.
Co-authored-by: multica-agent <github@multica.ai>
* feat(project): add update endpoint + label-shadow guard for project_resource (MUL-2662)
Addresses the Elon review on PR #3263:
- Add PUT /api/projects/{id}/resources/{resourceId} with sqlc query,
matching handler, CLI `project resource update`, and a new
EventProjectResourceUpdated WS event. resource_type stays immutable;
ref/label/position are all individually optional.
- Catch same-project (daemon_id, local_path) collisions where only the
embedded label differs — the row-level UNIQUE only matches the full
ref JSON, so a label typo would otherwise let the same working
directory bind twice.
- Tests cover the update lifecycle (label-only / ref / clear / 404 /
invalid path) and the label-shadow conflict on both create and
update; the in-place rename still succeeds because the conflict
scan ignores the row being edited.
Incidental: regenerating sqlc picked up a missing skills_local scan in
UpdateAgentCustomEnv that drifted in from #3200.
Co-authored-by: multica-agent <github@multica.ai>
* fix(project): close bundled-create label-shadow gap + merge resource_ref on CLI update (MUL-2662)
Two follow-ups from MUL-2662 review round 2:
- CreateProject inline resources path now dedupes local_directory entries on
(daemon_id, local_path) before opening the transaction. The DB-level
UNIQUE(project_id, resource_type, resource_ref) constraint only fires on a
full JSON match, so two rows with the same target but different `label`
would otherwise slip past. Standalone POST/PUT already cover this via
findLocalDirectoryConflict; bundled create was the missing surface.
- `multica project resource update` now seeds resource_ref from the existing
row before applying per-type shortcut flags, so `--default-branch-hint x`
on its own no longer constructs a payload missing `url` (which the server
400s on). Local_directory partial edits get the same merge behavior.
Co-authored-by: multica-agent <github@multica.ai>
* feat(desktop): local_directory project_resource UI (MUL-2665) (#3273)
* feat(desktop): local_directory project_resource UI (MUL-2665)
First UI surface for the local-working-directory flow tracked in MUL-2618.
Lets users on the desktop pin a project to an existing folder on this
machine; web stays read-only since the per-daemon check can't be done in
the browser.
What's new for the renderer:
- ProjectResourcesSection grows a desktop-only "Add local directory"
button next to the existing GitHub-repo popover. Clicking it opens
Electron's native folder picker, validates the path through a new
IPC pair (existence + r/w), and submits a project_resource of
resource_type=local_directory with daemon_id pulled live from
daemonAPI.getStatus.
- LocalDirectoryRow renders the rename pencil + path tooltip, and
greys out when ref.daemon_id != this machine's daemon_id (with a
"only available on the machine that registered this directory"
tooltip). Delete stays enabled so users can drop stale registrations
from any device.
- LocalDirectoryHint sits above the issue-detail comment composer and
shows "Agent will work in-place at {label} ({path})" when the issue's
project has a local_directory matching this daemon. Hidden on web.
- TaskStatusPill picks up a new "waiting_for_directory_release" stage
that the daemon will publish when it dequeues a task but can't
acquire the path lock. The render is in place now so the daemon
sibling subtask can wire the status string without an additional UI
PR.
Plumbing:
- @multica/core/types gains LocalDirectoryResourceRef +
UpdateProjectResourceRequest, and the api client gets the matching
PUT method backed by the server endpoint that landed in
|
||
|
|
744b474199 |
revert(agent): remove per-agent local skill toggle (MUL-2603) (#3286)
* Revert "feat(agents): hide skills_local toggle for runtimes that don't honour it (MUL-2603) (#3276)" This reverts commit |
||
|
|
6bc3d14eb3 |
fix(daemon/execenv): refresh stale Codex config copies across env reuse (MUL-2646) (#3268)
* fix(daemon/execenv): refresh stale Codex config copies across env reuse (MUL-2646)
`copyFileIfExists` previously short-circuited whenever the per-task
`codex-home/{config.toml,config.json,instructions.md}` already existed,
so once the files were seeded at first Prepare they were never refreshed
again — even though `Reuse()` calls `prepareCodexHomeWithOpts` on every
resume. A user who rotated their Codex `~/.codex/config.toml` between
runs (e.g. switching the active `[model_providers.X]` `base_url`, or
pointing `env_key` at a freshly rotated API key) kept reading the stale
per-task copy on session resume. Codex then issued requests to the new
URL using the old key and the API rejected the token.
Treat any existing `dst` as something to drop and re-copy from the
current shared source, mirroring the symlink path that already refreshes
`auth.json` (#2126). The daemon-managed sandbox / multi-agent / memory
blocks are applied via marker-bracketed idempotent passes after the
copy, so a re-copy + re-ensure cycle preserves them.
Co-authored-by: multica-agent <github@multica.ai>
* fix(daemon/execenv): drop per-task Codex copy when shared source removed (MUL-2646)
Extend the MUL-2646 fix to the deletion arm of "sync the shared source":
`syncCopiedFile` (renamed from `copyFileIfExists`) now also removes the
per-task `dst` when the shared `src` is absent. The prior version
short-circuited on missing src and left `config.toml` / `config.json` /
`instructions.md` from the previous Prepare lingering in the per-task
home — so a user who removed a provider by deleting `~/.codex/config.toml`,
or pulled `config.json` / `instructions.md` out of the shared home, would
keep replaying the stale copy on session resume.
For `config.toml` the subsequent `ensureCodex{Sandbox,MultiAgent,Memory}Config`
passes recreate the file with only the daemon-managed default blocks, so
removing the shared file cleanly drops every user-managed
`[model_providers.X]` / `model_provider` line. For `config.json` and
`instructions.md` there is no daemon default, so they disappear in
lockstep with the shared source.
Adds `TestPrepareCodexHome_DropsCopiedConfigWhenSharedSourceRemoved`
covering the new path, and extends the refresh-arm test to assert the
multi-agent / memory marker blocks are still present after the copy is
refreshed.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
960befa56f |
feat(agent): per-agent toggle to isolate host-machine skills (MUL-2603) (#3200)
* feat(agent): per-agent toggle to isolate host-machine skills (MUL-2603)
Adds an agent-scoped `skills_local` switch ("ignore" default / "merge") so
shared agents stop inheriting the operator's user-global Claude skill
directory. A single broken local skill on one operator's machine was
crashing the Claude CLI before it ever read stdin — the daemon saw a
"broken pipe" with no recoverable signal (GitHub #3052).
- DB: migration 108 adds `agent.skills_local` (NOT NULL DEFAULT 'ignore'),
with sqlc CreateAgent/UpdateAgent updates and handler validation.
- Claude runtime: when the agent is in "ignore" mode the backend points
CLAUDE_CONFIG_DIR at an empty per-task scratch dir under the task cwd
(fallback: OS temp), strips any inherited override, and cleans up after
the run. Workspace skills under `{cwd}/.claude/skills/` still load.
"merge" preserves the legacy inherit-from-machine behavior; Codex and
other isolated backends are no-ops.
- UI: new Skills toggle in the Create Agent dialog and the Agent → Skills
tab, with EN/zh-Hans copy and SkillsLocalToggle shared between the two.
- Tests: unit coverage for the new env helper, isolation dir lifecycle,
full Claude execute paths (ignore + merge), and the handler tristate
contract. Existing skills-tab test updated for the new copy.
- Docs: updated `/skills` docs (EN + ZH) and added a 0.3.7 changelog entry
in the landing-page i18n.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): preserve claude login + validate skills_local input (MUL-2603)
Address Elon's review on PR #3200:
1. Skill isolation no longer drops the operator's Claude login. The
per-task scratch dir now mirrors every entry under `~/.claude/`
as symlinks except `skills/`, so `.credentials.json`, settings,
plugins, etc. reach the CLI exactly as on the host while the
user-global skills directory stays hidden. Without this, default
`ignore` would have broken every Claude agent on a non-API-key
host the moment migration 108 landed.
2. Internal CreateAgent callers (agent_template, onboarding_shim)
now set `SkillsLocal: "ignore"`. The Go zero value was about to
trip the migration-108 CHECK constraint and 500 template /
onboarding agent creation.
3. Create / update handler validation no longer normalizes garbage
to "ignore". The strict 400 path is now reachable on bad client
input; the drift-safe `normalizeSkillsLocal` stays on the read
side only.
UI copy + docs clarified that the toggle is Claude-only; other
runtimes ignore the setting.
Verification:
- `go test ./...` green (full suite locally).
- `pnpm --filter @multica/views exec vitest run agents/components/tabs/skills-tab.test.tsx` green.
- Handler DB-backed tests still skip locally without docker (same
as Elon's run) — CI will validate the create / update paths
against migration 108.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): mirror effective claude config dir with windows fallback (MUL-2603)
Address Elon's second-round review on PR #3200:
1. The per-task scratch dir now mirrors the *effective* host Claude
config dir, not unconditionally `~/.claude/`. Precedence: agent
`custom_env` CLAUDE_CONFIG_DIR > parent process env > `~/.claude/`.
Without this, an operator who pinned Claude at a managed install
(custom env CLAUDE_CONFIG_DIR) would get the wrong credentials in
the scratch dir, because `buildClaudeEnv` strips that env before
handing it to the child. We resolve the source up front and feed
it to the mirror, so the override env still points at the right
bytes.
2. Mirror entries now go through platform-aware linkers. On Windows
without Developer Mode / admin, `os.Symlink` is denied, which
previously left the scratch dir empty and broke Claude Code auth
on default `ignore`. The new helpers try symlink first, then fall
back to a directory junction (`mklink /J`) for dirs or a hardlink
(same-volume content share) / copy for files. Mirrors the
execenv/codex_home_link_windows.go pattern.
3. Tests:
- `TestResolveHostClaudeConfigDir` locks in the custom_env >
parent_env > `~/.claude` precedence.
- `TestNewIsolatedClaudeConfigDirMirrorsCustomHostDir` confirms
the scratch dir picks up `.credentials.json` from a synthetic
custom host dir, proving the source resolution actually
propagates into the mirror.
- `TestNewIsolatedClaudeConfigDirEmptyHostIsNoop` documents the
env-var-auth-only case (no host source ⇒ empty scratch dir).
- `TestMirrorHostClaudeExceptSkillsWith_FallbackWhenSymlinkFails`
exercises the Windows-no-Developer-Mode path via the new
`mirrorHostClaudeExceptSkillsWith` seam, asserting credentials
and sub-dir children still reach the scratch dir after the
symlink stand-in fails.
- `TestMirrorHostClaudeExceptSkillsWith_PropagatesFirstLinkError`
confirms callers see the per-entry error when even fallback
fails (so the warn-log fires on broken Windows installs).
- `TestCopyFileRoundTrip` covers the last-resort copy fallback
and its EXCL no-overwrite contract.
- `TestClaudeExecuteIsolatesUsesCustomEnvSource` is the
end-to-end check: an agent with custom_env CLAUDE_CONFIG_DIR
reads its credentials from the pinned dir, not `~/.claude/`.
4. Docs: `apps/docs/content/docs/skills.{mdx,zh.mdx}` updated to
describe the effective-source resolution and the Windows
fallback chain so the docs match the runtime behaviour.
Verification:
- `go test ./...` green (full server suite locally, including
`pkg/agent` 23 cases covering the new + existing isolation
paths).
- `GOOS=windows GOARCH=amd64 go vet ./pkg/agent/...` and
`go test -c -o /dev/null` both compile clean, confirming the
Windows-tagged linker file builds.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): default skills_local to merge to preserve legacy behavior (MUL-2603)
Per Bohan's product decision on PR #3200, the per-agent host-skill toggle
defaults to "merge" — the pre-MUL-2603 inherit-from-machine behavior —
so existing personal workflows that rely on locally installed Claude
Skills keep working unchanged. Agent owners explicitly opt into "ignore"
when they need to harden a shared agent against a broken local skill on
one operator's machine (GitHub #3052).
Also audited all 11 runtimes for user-global skill discovery paths and
documented the scope of the toggle. Only Claude reads a user-global
`~/.claude/skills/`; Codex isolates via `CODEX_HOME`, the ACP backends
(Hermes / Kimi / Kiro) and the JSON-stream backends (Copilot / Cursor /
Gemini / Pi / OpenCode / OpenClaw) anchor discovery to the task workdir
and never read a user-global skill directory. UI copy and docs now say
"for runtimes that support it (currently Claude Code)" everywhere so
the scope is explicit.
Changes:
- Migration 108: column default flipped to 'merge'.
- Handler CreateAgent: missing field → "merge"; explicit "ignore" /
"merge" still validated, garbage still 400.
- normalizeSkillsLocal: drift-safe coercion now lands on "merge" for
anything that isn't the exact literal "ignore".
- agent_template.go / onboarding_shim.go: internal CreateAgent callers
send "merge" instead of "ignore" to match the new default.
- Claude runtime (`claude.go`): isolate-mode gate flipped from
`SkillsLocal != "merge"` to `SkillsLocal == "ignore"`, so "" (legacy
daemons / older clients) and "merge" both walk `~/.claude/` directly.
- Create Agent dialog + Skills tab: toggle defaults to on (merge); only
duplicate of an explicit "ignore" agent carries through. The
isolation opt-in is now `skills_local: "ignore"` when the user flips
off; "merge" is omitted from the request body.
- i18n (EN + zh-Hans): copy reframed — "On (default) — merged"; "Off —
ignored. Recommended for shared agents".
- Docs (`/skills`, `/guides/agents.zh`): describe new default and
enumerate which runtimes act on the toggle.
- Landing changelog 0.3.7: retitled "Per-Agent Local-Skill Toggle"; note
the on-by-default behavior + off-to-isolate framing.
- Tests:
- `TestClaudeExecuteIsolatesHostSkillsWhenIgnoreOptedIn` replaces the
old by-default isolation case (now requires explicit "ignore").
- New `TestClaudeExecuteDefaultModeKeepsHostConfigDir` locks in that
default ExecOptions preserve the host CLAUDE_CONFIG_DIR.
- `TestClaudeExecuteIsolatesUsesCustomEnvSource` now explicitly opts
into "ignore" mode.
- Handler tests: omitted → "merge"; explicit "ignore" round-trips;
preserve-existing test seeds "ignore" and asserts "merge" flip-back.
- `TestNormalizeSkillsLocal_DriftStaysSafe`: only literal "ignore"
maps to ignore; everything else → "merge".
- `skills-tab.test.tsx`: toggle ON by default; flip OFF when agent
opted into "ignore". Intro-text matcher anchored to a more specific
phrase so it no longer collides with the toggle hint copy.
Verification:
- `go test ./...` green (full server suite locally).
- `GOOS=windows GOARCH=amd64 go vet ./pkg/agent/...` and
`go test -c -o /dev/null` both compile clean (windows-tagged linker
file still builds).
- `pnpm typecheck` green across all packages and apps.
- `pnpm --filter @multica/views test` 88 files / 771 tests green.
- `pnpm --filter @multica/core test` 43 files / 390 tests green.
- Handler DB-backed tests still skip locally without docker; CI will
validate the create / update paths against migration 108.
Co-authored-by: multica-agent <github@multica.ai>
* chore(landing): drop 0.3.7 changelog entry from this PR (MUL-2603)
The landing-page release notes belong in a separate release-prep PR, not in the feature PR.
Co-authored-by: multica-agent <github@multica.ai>
* fix(agent): propagate skills_local=ignore to codex user-skill seed (MUL-2603)
Make the per-agent skills_local toggle real for Codex too, not just Claude.
Previously the toggle was only consumed by the Claude backend, while the
daemon's execenv layer always seeded Codex's per-task CODEX_HOME with the
host machine's user-installed skills from ~/.codex/skills/. A shared Codex
agent with skills_local=ignore could still inherit a broken local skill
from one operator's machine.
Now: PrepareParams/ReuseParams carry SkillsLocal; hydrateCodexSkills
skips seedUserCodexSkills when SkillsLocal == "ignore" so the per-task
CODEX_HOME exposes only workspace skills to the codex CLI. Default
("merge", or empty from older servers/clients) preserves existing
inherit-from-machine behavior. UI / docs are updated to reflect the
contract honestly: Claude Code and Codex honor the toggle; other
runtimes (Hermes / Kimi / Kiro / Copilot / Cursor / Gemini / Pi /
OpenCode / OpenClaw) leave $HOME untouched and discover user-level
skills natively, so the toggle is a no-op for them today.
New tests: TestPrepareCodexSkillsLocalIgnoreSkipsUserSeed,
TestPrepareCodexSkillsLocalMergeSeedsUserSkills, and
TestReuseCodexSkillsLocalIgnoreSkipsUserSeed cover Prepare(ignore),
Prepare(merge), and the toggle-flip-on-reuse path.
Co-authored-by: multica-agent <github@multica.ai>
* docs(skills): scope skills_local toggle copy to Claude Code + Codex (MUL-2603)
Off-state hint and Skills tab intro now explicitly call out Claude Code +
Codex as the only runtimes that honor the toggle, with "other runtimes
ignore this setting" wired into both states (en + zh-Hans), so users on
non-Claude/Codex agents don't read "Off" as runtime-wide isolation.
Docs (skills.mdx, skills.zh.mdx, guides/agents.zh.mdx) stop describing
Hermes / Kimi / Gemini / Copilot / Cursor / Pi / OpenCode / OpenClaw / Kiro
as having native user-level skill discovery; the daemon simply does not
manage user-level skill discovery for those runtimes today, and the toggle
is a no-op regardless of where it is set.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
cd71b0fe05 |
fix(daemon): disable Codex native auto-memory in per-task config.toml (#3202)
Codex CLI's auto-memory subsystem writes summaries to `$CODEX_HOME/memories/raw_memories.md` and `state_*.sqlite`, then reads them back on the next turn. The daemon never cleared these files across Reuse(), and Codex CLI may also pull from user-level `~/.codex/memories/` entirely outside the per-task isolation. Either path leaks unrelated context into new Multica tasks — multica#3130 saw `D:\Project\MoHaYu\ WowChat` Raw Memories injected into a brand-new issue's first turn. Write a daemon-managed block into the per-task `config.toml` that sets `features.memories = false`, `memories.generate_memories = false`, and `memories.use_memories = false`. Codex then neither writes nor reads its memory subsystem regardless of where the residual files live. The user's global `~/.codex/config.toml` is never touched. Pattern mirrors `ensureCodexMultiAgentConfig`: idempotent managed-block upsert, two TOML layout variants (root dotted-key vs. inside a `[features]` / `[memories]` table) to satisfy strict toml-rs parsing, and a `MULTICA_CODEX_MEMORY` env-var escape hatch. MUL-2598 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
8e9df90d32 |
feat: include repo description in agent brief (#3203)
Add Description field to RepoData structs so that workspace repo descriptions (set via the settings UI) are preserved through normalization and rendered in the agent brief as: - <url> — <description> When no description is set, the existing format is unchanged. Closes MUL-2610 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
a55c03a0b3 |
fix(agent): inject Workspace Context into agent brief (MUL-2542) (#3078)
* fix(agent): inject Workspace Context into agent brief (MUL-2542) The per-workspace `workspace.context` field (Settings → General) was stored in the DB but never reached the agent prompt. Plumb it from the workspace row through the claim response, the daemon's Task struct and TaskContextForEnv, and render it as `## Workspace Context` in the meta brief above `## Available Commands`. Heading is skipped when the field is empty so workspaces that haven't set a context don't see a bare header. Applies to every task kind — issue, comment, chat, autopilot, quick-create — so the shared system prompt is consistent regardless of trigger source. Co-authored-by: multica-agent <github@multica.ai> * chore(server): gofmt files touched by workspace-context injection Run gofmt on the files that buildWorkspaceContext injection touched. Cleans up composite-literal alignment in execenv task context and struct-tag alignment in Task / AgentTaskResponse / RegisterRequest. No behavior change. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> Co-authored-by: J <agent-j@multica.ai> |
||
|
|
c967ae0e0e |
feat(issues): platform-owned parent notify on child done (MUL-2538) (#3055)
* feat(issues): platform-owned parent notify on child done (MUL-2538)
When a child issue transitions from a non-done status into `done` and has
an open parent, the server now posts a top-level platform-generated
comment on the parent itself. Replaces the agent-prompt rule shipped in
PR #2918, which produced self-mention loops, planner ping-pong, and
accidental `MUL-` prefix hardcoding because the agent did not always know
the workspace prefix.
- Migration 107 widens `comment.author_type` to allow `system`; the
zero UUID is used as the sentinel `author_id` (the column stays NOT
NULL, callers branch on `author_type === 'system'`).
- `Handler.notifyParentOfChildDone` fires from both `UpdateIssue` and
`BatchUpdateIssues`. Guards: prev status != done, new status == done,
parent set, parent not in `done`/`cancelled`. Bypasses the
CreateComment HTTP path so the assignee on_comment trigger and the
mention-trigger paths do not fire — the comment content carries only
the safe issue mention for the child, no `mention://agent/...` /
`mention://member/...` / `mention://squad/...` links.
- `runtime_config.go` downgrades the Parent/Sub-issue Protocol rule 1
to an explicit "do NOT post one yourself" guardrail; rule 2 (sub-issue
creation `--status todo` vs `backlog`) is unchanged.
- New handler test exercises the happy path, idempotency, reopen+done,
parent done/cancelled guards, and the no-parent case. Runtime-config
tests reassert the new wording and the banned strings from the prior
revision.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): isolate system comments + wire GH merge path (MUL-2538)
Addresses the two must-fix items from the PR #3055 second review:
1. The platform-generated `comment:created` event (author_type='system')
was running through the generic comment listeners, which (a) tried to
subscribe the zero-UUID author and (b) parsed @mentions from the body
for inbox notifications. Both subscriber_listeners and
notification_listeners now early-return on author_type='system' so the
event becomes a pure WS broadcast for the timeline — no inbox rows,
no transcluded-mention attack surface.
2. advanceIssueToDone (the GitHub merge auto-done path) only published
issue:updated and skipped notifyParentOfChildDone, so a child closed
via merged PR — the dominant completion path — left the parent
silent. The helper is now invoked on the same prev/updated pair, with
the existing guards (transition + parent state) protecting double-fire.
Tests:
- New cmd/server/notification_listeners_test:
TestNotification_SystemCommentSkipsInboxAndMentions (parent subscribers
and smuggled @mention targets stay quiet),
TestSubscriberSystemCommentDoesNotSubscribe (zero-UUID never reaches
AddIssueSubscriber).
- New internal/handler/github_test:
TestWebhook_MergedPR_ChildWithParent_NotifiesParent fires a real
pull_request closed-merged webhook against a child and asserts the
parent receives exactly one safe system comment with the workspace's
real identifier (no `mention://agent|member|squad` links).
Co-authored-by: multica-agent <github@multica.ai>
* fix(runtime): drop parent-notification guidance from agent brief (MUL-2538)
Per Bohan's product call on PR #3055: the platform now owns the
child-done parent notification, so the runtime brief should not mention
the parent-comment path at all — not as an instruction, not as a "do
not do it" guardrail. The previous revision kept rule 1 of the Parent /
Sub-issue Protocol as a "Do NOT post your own parent-notification
comment." sentence; that still puts the concept in front of the agent
every run, which is exactly what we are trying to avoid.
What changes:
- Delete the "Parent / Sub-issue Protocol" preamble and rule 1 from
buildMetaSkillContent. The remaining content — the `--status todo`
vs `--status backlog` rule for creating sub-issues — now lives in a
dedicated `## Sub-issue Creation` section, since the parent/child
framing it previously sat under is gone.
- The system comment on the parent stays exactly as in
|
||
|
|
ae530ef057 |
docs(runtime): tighten issue-metadata write bar (MUL-2507) (#3004)
The previous wording invited agents to pin too much: any opened PR, external link, or "fact future agents will want one-glance access to" was framed as worth writing, with no explicit upper bound. In practice this caused metadata bags to accumulate single-run details and description-summary noise instead of the small set of repeatedly-read values the feature was designed for. Rework the agent runtime brief and the CLI docs to lead with the bar: write a key only when it is materially important AND likely to be re-read by future runs on the same issue. "Most runs write zero new keys" is now stated as the expected case, and the workflow exit step is rewritten to mirror the same gate. Recommended-key list, safety boundaries, and stale-key cleanup are preserved so the locked-in test anchors still pass. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
0c767c0052 |
feat(issues): per-issue metadata KV (MUL-2017) (#2845)
* feat(issues): per-issue metadata KV (MUL-2017)
Adds a small JSONB KV map to every issue for agent pipeline state (attempts,
PR number, pipeline status, ...). Keys match a narrow regex, values are
primitives (string / number / bool), capped at 50 keys per issue and 8KB
per blob. Defense-in-depth via two CHECK constraints (object shape + size).
All mutations are single-key atomic (jsonb_set / `- key`). `UpdateIssue`
intentionally does NOT touch metadata: a whole-blob overwrite would race
with concurrent agent writes.
GET /api/issues/:id/metadata
PUT /api/issues/:id/metadata/:key body: { "value": <primitive> }
DELETE /api/issues/:id/metadata/:key
Containment filter on list: GET /api/issues?metadata=<json-object> uses
PG `@>` against a `jsonb_path_ops` GIN index. Mirrored across ListIssues,
CountIssues, ListOpenIssues, and the hand-rolled ListGroupedIssues SQL so
CLI/API and UI grouped views stay consistent.
CLI: multica issue metadata {list,get,set,delete}
multica issue list --metadata key=value (repeatable, AND)
set has --type to override the default value-sniffing
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): metadata test bugs + wire realtime + read-only display (MUL-2017)
- Fix two failing handler tests blocking backend CI:
- reset decode target after delete so map merge does not mask removal
- url.PathEscape the key segment so spaces no longer panic NewRequest
- Wire issue_metadata:changed end to end so the detail / list / my-issues
caches stay in sync with set/delete events (other tabs, CLI writes).
- Add a read-only Metadata strip to the issue detail sidebar; hidden when
the issue has no keys so it stays quiet in the common case.
Co-authored-by: multica-agent <github@multica.ai>
* feat(runtime): teach agents to read/write issue metadata (MUL-2017)
Add an `## Issue Metadata` section to the runtime brief plus a
`metadata list` step on entry and a `metadata set`/`delete` step on
exit. Section only emits when the task carries an issue id (comment- or
assignment-triggered); chat / quick-create / run-only autopilot stay
clean so they don't fire failing CLI calls.
Co-authored-by: multica-agent <github@multica.ai>
* fix(issues): bump metadata migration to 105 and drop attempts as example (MUL-2017)
main is now at 104_drop_runtime_timezone; the migrator picks
LatestVersion() by sorted filename, so a slot before the tail would
let DBs that have already run 099–104 think they're up-to-date while
the issue.metadata column is missing — runtime would then fail with
column does not exist. Renumbering to 105 puts the migration at the
tail and forces it to run.
Also drop attempts as a positive example across docs/code comments and
test fixtures — the runtime instruction prompt already lists it under
"What NOT to pin" (runtime bookkeeping). Replace with pr_number, which
is in the recommended-keys set, so docs/tests speak the same language
as the prompt.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
7f9e4e829d |
feat(comments): thread-internal --tail pagination + reply cursor (MUL-2421) (#2846)
* feat(comments): thread-internal pagination via --tail + reply cursor (MUL-2421) Long threads inside a single issue still forced agents to read every reply once they used --thread, even after MUL-2387 fixed cross-thread noise. This adds reply-level paging so a 200-reply thread can be navigated tail-first without dragging the whole conversation into prompt context. - New SQL query ListThreadCommentsForIssuePaged: same recursive root walk as the legacy thread query, but caps reply count and supports an (created_at, id) composite cursor. Root is unconditional — even tail=0 emits it so the reader keeps the "what is this thread about" context. - Handler ListComments: parses `tail` (non-negative, ThreadTailSet flag preserves the tail=0 intent), threads it through to the paged query, and re-uses X-Multica-Next-Before / X-Multica-Next-Before-Id for the reply cursor. Cursor's meaning is now context-dependent: thread cursor under --recent, reply cursor under --thread + --tail. - CLI: new --tail flag (only valid with --thread; mutually exclusive with --recent), reply-cursor semantics for --before / --before-id when paired with --thread + --tail, stderr label flips to "Next reply cursor" so an operator copy-pasting the cursor knows which scope it scrolls. - Tests cover the new contract: tail=N keeps newest N + root, tail=0 is root-only, anchor on a nested reply still walks up, reply cursor scrolls older replies page-by-page, since combined with tail filters after the cut, and the negative-flag-combination matrix. Out of scope: prompt template update to hint at `--thread <id> --tail 30` on long threads — separate follow-up per the issue. Co-authored-by: multica-agent <github@multica.ai> * fix(comments): only emit reply cursor when older reply exists (MUL-2421) The thread-tail path emitted `X-Multica-Next-Before` whenever the page filled to exactly the requested reply count, even when there was nothing older to scroll to. So `--thread <root> --tail 3` on a thread with exactly 3 replies sent a cursor that, when followed, returned just the root — a wasted round-trip that surfaced as a phantom "older replies" affordance in the agent prompt. Switch to a `reply_limit + 1` probe: ask the SQL for one extra row, trim the oldest overflow before responding, and only emit the cursor when an older reply actually existed. The exact-boundary case (replyCount == tail with no overflow) now returns no cursor. Also documents `--thread/--tail/--recent/--before` and the cursor semantics in CLI_AND_DAEMON.md, which was the second must-fix in the MUL-2421 review. Co-authored-by: multica-agent <github@multica.ai> * fix(comments): suppress reply cursor when --since covers older replies (MUL-2421) In the thread + tail + since path the server still emitted a reply cursor whenever there was an older reply on disk, regardless of `since`. If the oldest retained reply on the page was already `<= since`, every older reply was guaranteed to be filtered out too, so the next page only ever returned the root — wasting round-trips until the agent walked the whole pre-`since` history. Mirror the recent + since suppression: when `replies[0].CreatedAt <= since`, drop the cursor. Test covers the exact case from Elon's review: tail=2 overflow, body keeps a fresher reply, but the cursor target (oldest retained reply) is already past `since` — header must be empty. Co-authored-by: multica-agent <github@multica.ai> * feat(prompt): default comment-trigger reads to --thread --tail 30 (MUL-2421) Comment-triggered agents previously defaulted the trigger-thread read to the unbounded `--thread <id> --output json`, which dumps the full thread into the prompt — exactly the kind of context bloat MUL-2387 fixed at the cross-thread layer but never bounded inside a single thread. Use the new `--tail` flag landed earlier in this PR (server + CLI) as the default for both the per-turn prompt and the runtime-config Workflow: - `--thread <trigger-id> --tail 30 --output json` is the new default. Root is always included so "what is this about" context survives. - If 30 replies aren't enough, the prompt now spells out the reply cursor: re-feed the stderr `Next reply cursor: --before <ts> --before-id <reply-id>` pair back to walk older replies. - `--recent 20` stays as the cross-thread background fallback, with an explicit callout that the same `--before` / `--before-id` flags walk *threads* (not replies) in that mode. - Available Commands core line now surfaces `--tail N` and both stderr cursor labels so non-workflow callers also discover the flag. - `--since` callouts reflect the post-MUL-2421 combinable mode names (`--thread --tail` / `--recent`). Tests (`prompt_test.go`, `execenv_test.go`) pin the new defaults and add a regression guard against the unbounded `--thread` recipe sneaking back in. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
aeb284cbeb |
feat(runtime): teach agents the parent/sub-issue protocol (MUL-2338) (#2918)
* feat(runtime): teach agents the parent/sub-issue protocol (MUL-2338) Adds a Parent / Sub-issue Protocol section to the runtime brief built by `buildMetaSkillContent`, emitted whenever the agent is running on a real Multica issue (assignment- or comment-triggered). Two behaviors are now documented for every issue-bound agent: - A. When wrapping up a child issue, post the final result and switch to `in_review` on this issue first, then post a single top-level comment on the parent. Mention the parent assignee only when it is another agent on a still-open parent — never self-mention, never @ member / squad, never re-trigger a `done` / `cancelled` parent. - B. When creating sub-issues, choose `--status backlog` for sub-issues that must wait and `--status todo` for the one to start immediately; promote with `multica issue status <id> todo` when its turn comes. The signal is explicitly framed as best-effort — no server-side state sync, no claim of a guaranteed handshake. The section is skipped for chat, quick-create, and run-only autopilot runs, which have no parent/child semantics. Tests in runtime_config_test.go assert that the section is present in both issue workflows, absent in the three non-issue modes, and that the wording does not introduce a non-existent `multica issue list --parent` command or promise a reliable handshake. Co-authored-by: multica-agent <github@multica.ai> * fix(runtime): split Step A of parent/sub-issue protocol by trigger type (MUL-2338) Comment-triggered runs were inheriting an unconditional `multica issue status <this-issue-id> in_review` from Step A, which conflicts with the comment-triggered workflow rule "Do NOT change the issue status unless the comment explicitly asks for it" (Elon's blocking review on PR #2918). Step A now branches on trigger type: - Assignment-triggered: keep "post final results + flip in_review". - Comment-triggered: complete the reply per the existing workflow rule, only flip status when the triggering comment asked for it, and gate the parent-notification steps on actually closing out child work. Tests lock the boundary: comment-triggered briefs must not contain the unconditional in_review command, must echo the existing status guardrail inside Step A, and must spell out the "closing out" gate. Assignment-triggered briefs still carry the unconditional flip. Co-authored-by: multica-agent <github@multica.ai> * fix(runtime): simplify parent/sub-issue mention rule to always @ parent assignee (MUL-2338) Per Bohan's directive on PR #2918: the per-case mention table (same agent / member / squad / closed parent) is overkill prompt complexity. Replace it with a single rule: always @mention the parent's assignee using the URL that matches assignee_type. The platform's existing run dedup handles re-triggers, and a single rule is easier for agents to follow predictably. Preserves the existing comment-triggered boundary (Step A still does NOT add an unconditional in_review flip on comment-triggered runs). Co-authored-by: multica-agent <github@multica.ai> * refactor(runtime): compress parent/sub-issue protocol to 3-rule convention (MUL-2338) Drop the spec-flavored A/B sub-headings and per-case mention table; keep three numbered rules (close out child, notify parent, pick backlog vs todo) plus a one-line best-effort preamble. The comment-triggered branch still re-asserts the "do not change status unless asked" guardrail and gates parent notification on actually closing out child work; the assignment-triggered branch still flips to `in_review`. Section is now 7 lines instead of 29. A new TestParentSubIssueProtocolIsCompact guards the ≤10-line ceiling so this stays a convention, not a spec. Co-authored-by: multica-agent <github@multica.ai> * fix(runtime): make sub-issue creation rule unconditional in parent/sub-issue protocol (MUL-2338) Elon's review on PR #2918: the preamble previously gated all three rules on the current issue having `parent_issue_id`, but rule 3 (creating sub-issues) needs to reach top-level parents that have no parent themselves — that is exactly where the `todo` vs `backlog` decision matters most. Move the gate from the preamble onto rules 1 and 2 per-rule; rule 3 now applies to any issue-bound run. Section stays at 7 newlines (≤10). Co-authored-by: multica-agent <github@multica.ai> * refactor(runtime): unify parent/sub-issue protocol as mechanism description (MUL-2338) Drop the if/else split between assignment- and comment-triggered runs in the Parent / Sub-issue Protocol section: both runs now read the same two-rule description of how the parent/child mechanism works. The comment-triggered workflow rule "Do NOT change the issue status unless the comment explicitly asks for it" naturally short-circuits the parent notification (no status flip → not closing out the child → skip), so the protocol no longer needs to branch on TriggerCommentID. Tests collapse the two trigger-specific cases into one parameterized test, and the assignment vs comment status-flip invariants are now anchored on the real workflow command (with substituted issue id) instead of the protocol's removed `<this-issue-id>` placeholder. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
2ad1cd8ff8 |
feat(profile): user profile description injected into agent brief (MUL-2406)
## Summary Adds per-user `profile_description` so coding agents have cheap, durable context about who is asking. v1 per the brief Xeon locked in on [MUL-2406](mention://issue/63a7247c-4f6a-42cf-90d1-7c746e77158a): - **DB** — `user.profile_description TEXT NOT NULL DEFAULT ''` (migration 096). 2000-rune cap enforced server-side. No nullable / privacy state to manage. - **API** — `PATCH /api/me` accepts the field; `UserResponse` always emits it. Client wraps `updateMe` in a lenient `UserSchema` + `EMPTY_USER` fallback per CLAUDE.md API Response Compatibility. - **UI** — Settings → Account gains an "About you" textarea with live `n/2000` counter, `maxLength` guard, and a localized too-long error (EN + zh-Hans). - **CLI** — `multica user profile get` / `multica user profile update` with `--description / --description-stdin / --description-file / --clear`, mirroring the existing `issue comment add` input-mode menu. - **Daemon injection** — claim handler resolves the runtime owner and stamps `requesting_user_name` + `requesting_user_profile_description` on the task. `buildMetaSkillContent` emits `## Requesting User` between `## Agent Identity` and `## Available Commands`, blockquoted and framed as background context. The block is omitted entirely when the description is empty (no token cost when unused). Brief is written **once per task** via `CLAUDE.md` / `AGENTS.md`, not the per-turn prompt — same path the agent already reads for identity, so no extra per-turn cost. ## Test plan - [x] `go build ./...`, `go vet ./...`, `go test ./internal/cli/ ./internal/daemon/ ./internal/daemon/execenv/ ./cmd/multica/` - [x] New brief tests: `TestBuildMetaSkillContentEmitsRequestingUser`, `TestBuildMetaSkillContentOmitsRequestingUserWhenEmpty` - [x] `pnpm typecheck`, `pnpm lint`, `pnpm test` (74 files, 644 tests pass) - [ ] Handler DB tests (`TestUpdateMe*`) require a migrated test DB — not runnable in this sandbox - [ ] Manual: open Settings → Account, set a description, confirm the next daemon-run agent's `CLAUDE.md` shows `## Requesting User` |
||
|
|
f120e0ef43 |
refactor(cli): tidy workspace subtree (MUL-2386) (#2866)
- Drop `workspace current`; `workspace get` (no args) already prints the current default workspace, so the two were doing the same thing. - Rename `workspace members` to `workspace member list` to free up the `member` namespace for future `add` / `remove` subcommands and align with the rest of the CLI's `<resource> <verb>` shape. - Add `--full-id` to `workspace list`, matching `project list`, `autopilot list`, and friends. Docs and the daemon prompt are updated to match. Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
9a577f3e11 |
fix(runtimes): anchor OpenCode skill + AGENTS.md discovery to task workdir (MUL-2416) (#2849)
* fix(runtimes): anchor OpenCode skill + AGENTS.md discovery to task workdir OpenCode resolves its project discovery root from `--dir` and `PWD` before falling back to `process.cwd()`. The daemon set `cmd.Dir = workDir` but never overrode the inherited `PWD`, so OpenCode walked from the daemon's shell directory and silently bypassed the per-task workdir — agents lost visibility into `.opencode/skills/` and `AGENTS.md`, falling back to whatever global skills the host had installed (MUL-2416). - Pass `opencode run --dir <workDir>` and override `PWD=<workDir>` in the child env so AGENTS.md walk-up + `.opencode/skills` project config scan both anchor on the task workdir. - Block `--dir` from custom args so user overrides cannot re-introduce the regression. - Plumb skill `description` from DB through service / daemon / execenv. `writeSkillFiles` synthesizes a YAML frontmatter block (`name`, optional `description`) when the stored content lacks one, since runtimes like OpenCode silently drop SKILL.md files without a parseable `name`. Existing frontmatter is preserved unchanged so upstream-imported skills (GitHub / ClawHub / Skills.sh) keep their hand-shaped metadata. Tests: - New fake-CLI test confirms argv carries `--dir <workDir>` and the child sees `PWD=<workDir>`. - New test confirms a user-supplied `--dir` in custom_args is dropped. - New execenv tests cover synthesized frontmatter and preservation of pre-existing frontmatter. Co-authored-by: multica-agent <github@multica.ai> * fix(runtimes): inject SKILL.md `name` when upstream frontmatter omits it Skills imported with frontmatter that sets `description` but leaves `name` implicit (relying on the directory slug, as common in GitHub/Skills.sh imports) still hit OpenCode's "no parseable name → drop" path because the DB Name fallback never made it into the SKILL.md body. ensureSkillFrontmatter now scans the existing block and, when name is missing or empty, prepends `name: <slug>` while preserving description, body, and any runtime-specific keys verbatim. Also tighten yamlEscapeInline to always double-quote so descriptions that look like YAML keywords (`null`, `true`, `[foo]`, `{x: y}`, `2024-01-01`) parse as strings rather than getting reinterpreted and rejected. Adds regression test for the nameless-frontmatter case and updates the existing OpenCode skill test for the always-quoted description format. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
e19f7967b9 |
feat(prompt): thread-first comment reads for agent runs (MUL-2387) (#2816)
* feat(prompt): thread-first comment reads for agent runs (MUL-2387) PR #2787 added --thread / --recent / --before / --before-id to the ListComments API and CLI but kept the agent prompt steering at the legacy "dump everything" recipe. On a long-running issue the flat dump burns context on chatter unrelated to the trigger; agents acting on the trigger want the trigger's thread first. Prompt updates: - Comment-triggered Workflow (runtime_config.go) now anchors step 2 on `multica issue comment list <issue-id> --thread <trigger-comment-id> --output json`. Fallback offers `--recent 20 --output json` with the stderr `Next thread cursor: --before <ts> --before-id <root-id>` line feeding the next-page cursor. `--since` is preserved and explicitly marked combinable with --thread / --recent. - Per-turn buildCommentPrompt (prompt.go) carries the same thread-first guidance so a Codex-style runtime that re-reads the per-turn message every iteration gets the same steering, even if it ignores the injected runtime config. - Assignment-triggered Workflow keeps the mandatory full-history rule (MUL-1124) but now also points at `--recent 20` as the long-issue alternative — this is the place that previously had no thread-aware guidance at all. - Default fallback prompt (no trigger comment, no chat, no autopilot, no quick-create) gains the same --recent hint without --thread (no comment to anchor on). - Available Commands core line surfaces the new flags so the discovery path matches the workflow guidance. Default CLI/API semantics are unchanged: the unparameterized list still returns the full chronological dump capped at 2000, --since still works on its own, and the desktop UI is untouched. Tests: - prompt_test.go: TestBuildPromptCommentTriggerPromotesThreadReads pins --thread <triggerID>, --recent 20, the stderr cursor phrasing, and the absence of the legacy "returns all comments" prose. - prompt_test.go: TestBuildPromptDefaultMentionsRecent guards the no-trigger fallback (mentions --recent, must NOT mention --thread). - execenv_test.go: TestInjectRuntimeConfigCommentTriggerThreadFirstReads asserts the comment-triggered Workflow steers at --thread/--recent, the Available Commands line surfaces the new flags, and the legacy "read the conversation (returns all comments...)" string is gone. - execenv_test.go: TestInjectRuntimeConfigAssignmentTriggerMentionsRecent keeps the mandatory full-history rule pinned AND asserts --recent is offered as the long-issue alternative. Also fixes the recent+since cursor nit Elon flagged in #2787's second review: when `since` empties the page, the `len(seenRoot) >= recentN` check used to emit a cursor anyway. Pagination walks threads in strictly decreasing last_activity_at — if every comment in this page is <= since, every older thread's last_activity is also <= since by transitivity, so the cursor would only invite the caller into a guaranteed-empty walk. Now suppressed; new tests pin both branches (suppressed when empty, retained when at least one row passes since). MUL-2387 Co-authored-by: multica-agent <github@multica.ai> * fix(comments): suppress recent+since cursor when head thread past since (MUL-2387) Previous suppression only tripped when the `since` filter emptied the page. That missed the mixed case Elon flagged in #2787's second review: the page keeps rows from fresher threads but the head (oldest-active) thread already sits at or before `since`, so every older page is guaranteed empty too. Predicating on `headLast <= since` covers both cases. Add a recent=2 + since fixture that pins the mixed scenario: root1 (last_activity = base+3m) is filtered out, root2 stays, and the cursor is suppressed even though the body is non-empty. Co-authored-by: multica-agent <github@multica.ai> * fix(prompt): clarify --recent is paging, not a replacement (MUL-2387) Address Elon's second-pass nit on #2816: the assignment-trigger workflow in runtime_config.go used "you may switch to --recent 20", which reads as a replacement for the mandatory full-history rule. Rephrase --recent as a paging strategy ("read the full history page-by-page, not a shortcut that replaces it") so it cannot conflict with the rule it lives next to. The default per-turn prompt in prompt.go opened with "If you need comment history" — that soft conditional contradicts the runtime workflow's mandatory read. Move it to a neutral "For comment history, follow the rule in your runtime workflow file" framing that defers to whatever the workflow says (mandatory for assignment, optional elsewhere) instead of encoding its own policy. Keep the runtime/prompt dual-layer fallback intact — different runtimes propagate the config file vs. the per-turn user prompt with varying fidelity, so both surfaces need the guidance. Tests pin the new phrasing against regression: - TestBuildPromptDefaultMentionsRecent now also forbids "If you need comment history" from sneaking back in. - TestInjectRuntimeConfigAssignmentTriggerMentionsRecent now also forbids "you may switch to" / "switch to `--recent" replacement phrasing. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
fe1ccb19c9 |
Revert "MUL-2324 conditionally inject non-core rule blocks (#2771)" (#2802)
This reverts commit
|
||
|
|
e8fb0efe3d |
MUL-2324 conditionally inject non-core rule blocks (#2771)
* feat(runtime): conditionally inject non-core rule blocks Co-authored-by: multica-agent <github@multica.ai> * fix(runtime): tighten mention rule triggers Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
58a76f6d96 |
fix(execenv): trim default runtime brief command list (MUL-2322) (#2769)
Trim the default runtime brief Available Commands to the agreed core set, including issue create/update, while keeping non-core commands discoverable through help. CI passed for backend and frontend. |
||
|
|
da7b33561e |
fix: make quick-create output prefix agnostic (#2604)
* fix: make quick-create output prefix agnostic * fix: remove quick-create prefix assumption from runtime config |
||
|
|
464201ba0d |
feat(execenv): native OpenClaw skill discovery via per-task config (MUL-2219) (#2628)
* feat(execenv): native OpenClaw skill discovery via per-task config
MUL-2213 stopped lying about native discovery and routed openclaw skills
to .agent_context/skills/ — a path openclaw's scanner never reads.
Multica skills attached to openclaw-backed agents were still invisible to
the runtime; the AGENTS.md fallback was only a documentation patch.
OpenClaw's skill scanner walks <workspaceDir>/skills/ (plus a few other
roots), and workspaceDir is resolved from the openclaw config file —
specifically agents.list[id].workspace → agents.defaults.workspace →
~/.openclaw/workspace. There is no CLI flag or env var override on the
agent runtime; the only knob is the config file.
This change wires a per-task synthesized config:
1. execenv.prepareOpenclawConfig deep-copies the user's existing
openclaw.json (priority: $OPENCLAW_CONFIG_PATH, else
~/.openclaw/openclaw.json), rewrites agents.defaults.workspace AND
every agents.list[].workspace to the task workdir, and writes the
result to {envRoot}/openclaw-config.json. Provider sections,
registered agents, model providers, gateway settings — everything
openclaw needs to actually start — are preserved as-is.
2. resolveSkillsDir for "openclaw" now points at {workDir}/skills/,
which is the first path openclaw scans under workspaceDir. Skills
written here are picked up natively.
3. daemon.go exports OPENCLAW_CONFIG_PATH={env.OpenclawConfigPath} on
the openclaw subprocess and adds OPENCLAW_CONFIG_PATH to the
custom_env blocklist so users cannot accidentally override it.
4. buildMetaSkillContent now lists openclaw alongside the
"discovered automatically" providers; the .agent_context/skills/
fallback line stays for gemini/hermes.
The new regression test TestPrepareOpenclawSkillWriteMatchesScanPath is
the one MUL-2219's DoD calls out: it resolves the workspaceDir the way
openclaw does (reading agents.defaults.workspace out of the synthesized
config) and proves {workspaceDir}/skills/<name>/SKILL.md is what Multica
actually wrote. The pre-MUL-2219 fix asserted "we wrote a file" without
checking the scanner would ever see it — which is how the dead drop into
.openclaw/skills/ landed in #2621's first commit.
Verified locally: minimum-viable synthesized config validates via
`openclaw config validate`, and `OPENCLAW_CONFIG_PATH=<path> openclaw
config get agents.defaults.workspace` returns the task workdir as
expected. MUL-2219
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): delegate openclaw config parsing to CLI and fail closed
Address Elon's must-fix on PR #2628: the previous implementation parsed
~/.openclaw/openclaw.json with encoding/json, which cannot read JSON5
or follow $include — the OpenClaw spec's actual format. When parsing
failed, prepareOpenclawConfig silently emitted a minimal config, which
could boot OpenClaw without the user's registered agents, model
providers, or API keys.
Two changes:
1. Delegate active-config-path resolution and config reading to the
openclaw CLI itself. `openclaw config file` locates the active
config (covering OPENCLAW_CONFIG_PATH / OPENCLAW_STATE_DIR /
OPENCLAW_HOME / default and the legacy chain), and the wrapper we
write uses $include to point at it so OpenClaw's own loader handles
JSON5, $include nesting, env-substitution, and secret refs. We read
only agents.list via `openclaw config get --json` to rewrite each
entry's workspace — secrets, comments, and includes in the user
config are never touched.
2. Remove the silent minimal-config fallback. Any CLI failure,
malformed output, or write error now surfaces as a hard error from
Prepare / Reuse. The only "synthesize minimal" path left is a fresh
install (CLI reports a path but the file doesn't exist), where
there is no user data to lose.
The per-task override still rewrites every agents.list[].workspace,
not just agents.defaults.workspace — this is intentional task
isolation, documented in prepareOpenclawConfig and the PR body. A
host-scope per-agent workspace would otherwise silently route the
scanner back to the user's shared workspace.
Cleanups Elon flagged in the same review:
- daemon.go inline-system-prompt comment no longer claims openclaw
ignores the task workdir; it does load it now, and the inline brief
is a belt-and-suspenders carryover for older releases.
- execenv.go openclaw block no longer references "skill file paths in
the inline brief" — the brief uses "discovered automatically".
Reuse() switches to a ReuseParams struct so the openclaw binary path
threads through alongside CodexVersion without a 6th positional arg.
MUL-2219
Co-authored-by: multica-agent <github@multica.ai>
* fix(execenv): grant OpenClaw $include cross-dir confinement for per-task wrapper
The per-task wrapper at envRoot/openclaw-config.json $includes the user's
active config (typically ~/.openclaw/openclaw.json), but OpenClaw confines
$include resolution to the wrapper file's directory unless the target's
parent is granted via OPENCLAW_INCLUDE_ROOTS. Without this, OpenClaw refuses
to follow the link at runtime and the wrapper boots with no user-registered
agents.
prepareOpenclawConfig now returns dirname(activePath) as IncludeRoot, and
the daemon prepends it to whatever the user already has in
OPENCLAW_INCLUDE_ROOTS via the new composeOpenclawIncludeRoots helper
(dedupes, drops empty segments, preserves user-configured roots). Fresh
install emits no $include and leaves the env var untouched.
Adds OPENCLAW_INCLUDE_ROOTS to the custom_env blocklist so a per-agent
override cannot strip the granted root.
Regression tests:
- TestPrepareOpenclawConfigWrapperLoadableUnderIncludeConfinement asserts
every $include target's dirname is covered by the IncludeRoot we surface.
- TestPrepareEnvironmentOpenclawWiresIncludeRoot covers the non-fresh-install
Environment wiring.
- TestComposeOpenclawIncludeRoots covers the daemon-side env composition
(preserve, dedupe, drop empties).
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: multica-agent <github@multica.ai>
|
||
|
|
f82a6adde9 |
fix(execenv): fall back OpenClaw skills to .agent_context/skills/ and stop claiming native auto-discovery (#2621)
* fix(execenv): write OpenClaw skills to .openclaw/skills/ for native discovery The OpenClaw provider was missing a case in resolveSkillsDir, so workspace skills attached to OpenClaw-backed agents fell through to .agent_context/ skills/ — a path the openclaw CLI never inspects. The result: agents created against the OpenClaw runtime saw zero of their loaded Skills in chat or task runs, even though the meta AGENTS.md content advertised them as auto-discovered. Mirrors the same per-provider mapping already in place for OpenCode, Copilot, Pi, Cursor, Kimi, Kiro. Also adds .openclaw to the repocache git-exclude list so the per-task skills directory does not pollute checked-out repos. MUL-2213 Co-authored-by: multica-agent <github@multica.ai> * fix(execenv): drop .openclaw/skills dead-drop write; flag openclaw as non-auto-discovery Reviewer (Elon) pointed out that {workDir}/.openclaw/skills/ is not in any OpenClaw skill discovery path. Confirmed by reading openclaw upstream (src/agents/skills/refresh.ts, src/agents/agent-scope-config.ts, src/cli/program/register.agent.ts): - OpenClaw scans <workspaceDir>/skills, <workspaceDir>/.agents/skills, ~/.openclaw/skills, ~/.agents/skills, bundled, and config skills.load.extraDirs. - workspaceDir is resolved from the openclaw config (per-agent workspace -> agents.defaults.workspace -> ~/.openclaw/workspace). It is NOT the cwd of the openclaw process. - There is no --workspace CLI flag on 'openclaw agent', and no OPENCLAW_WORKSPACE env var consumed at runtime. The only knob is the config file. So {workDir}/.openclaw/skills/ written by Multica is never seen by the openclaw runtime, and the meta AGENTS.md was lying to the agent by claiming auto-discovery. Reverts: - resolveSkillsDir: drop the openclaw case; falls back to .agent_context/skills/ (same path as hermes). - agentGitExcludePatterns: drop .openclaw; nothing is written there now. Also updates the openclaw branch in buildMetaSkillContent to point the agent at .agent_context/skills/ explicitly (alongside gemini/hermes), so loaded skills are at least referenced by path in the AGENTS.md context. The openclaw native loader still won't see them as installed skills. Native auto-discovery for openclaw needs per-task workspace integration (e.g. synthesized per-task config via OPENCLAW_CONFIG_PATH that overrides agents.defaults.workspace, or resolving the agent's actual configured workspace at exec time) — tracked as follow-up. MUL-2213 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
f6ac53a967 |
fix: squad leader no_action must not post comment on comment-triggered path (#2573)
PR #2564 only added IsSquadLeader handling to the assignment-triggered workflow path and the Output section. When a squad leader is triggered by a comment (the common case for re-evaluation), the comment-triggered workflow path had NO squad leader special handling, so the model still posted comments announcing no_action/silence. Changes: - runtime_config.go: Add IsSquadLeader check to comment-triggered step 4 with explicit prohibition against posting no_action announcement comments - runtime_config.go: Strengthen Output section from 'may exit silently' to 'MUST exit without posting any comment' with explicit DO NOT examples - runtime_config.go: Strengthen assignment-triggered step 5 similarly - prompt.go: Add squad leader no_action rule to per-turn comment prompt when trigger author is an agent and agent instructions contain the Squad Operating Protocol marker - Add tests for both the per-turn prompt and CLAUDE.md generation Fixes MUL-2168 Co-authored-by: multica-agent <github@multica.ai> |
||
|
|
7a1284128d |
fix: allow squad leader to exit silently on no_action without posting a comment (#2564)
The runtime prompt's Output section unconditionally required all tasks to post a comment via 'multica issue comment add', which conflicted with the squad leader protocol that says to 'exit silently' on no_action. Changes: - Add IsSquadLeader bool to TaskContextForEnv (detected via Squad Operating Protocol marker in agent instructions) - Relax the Output section and assignment-triggered workflow step 5 to allow squad leaders to exit with only a 'multica squad activity' call when the outcome is no_action Fixes MUL-2168 Co-authored-by: multica-agent <github@multica.ai> |