mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-10 10:26:46 +08:00
* feat(agent): add Kimi CLI as agent runtime Adds support for Moonshot AI's Kimi Code CLI (https://github.com/MoonshotAI/kimi-cli) as a new agent runtime, alongside Claude, Codex, OpenCode, OpenClaw, Hermes, Gemini, Pi, Cursor and Copilot. Kimi Code CLI implements the standard Agent Client Protocol (ACP) via the `kimi acp` subcommand, so the new `kimiBackend` reuses the existing hermesClient JSON-RPC transport in the agent package — only the binary, client identity, log prefix, and tool-name extraction differ. Wiring: - server/pkg/agent: new kimiBackend + kimi_test.go; registered in New(), LaunchHeader map, and the supported-types coverage test. - server/internal/daemon/config.go: probes `kimi` (overridable via MULTICA_KIMI_PATH / MULTICA_KIMI_MODEL). - server/internal/daemon/execenv: writes AGENTS.md as the runtime context file (Kimi reads AGENTS.md natively via /init), and writes skills under `.kimi/skills/` so they are auto-discovered by the project-level skill loader. - packages/views/runtimes: ProviderLogo gains a Kimi mark. Co-authored-by: Copilot <[email protected]> * feat(agent/kimi): support per-agent model selection via ACP set_model Wire Kimi into the model dropdown introduced in #1399: - ListModels gets a 'kimi' case that drives the same ACP initialize + session/new handshake as Hermes; both share a new discoverACPModels helper and parseACPSessionNewModels parser so future ACP backends only need a small provider entry. - kimiBackend now issues session/set_model after session/new when opts.Model is non-empty, mirroring the Hermes flow. Failures fail the task instead of silently falling back to Kimi's default model — silent fallback would hide that the dropdown pick wasn't honoured. Verified: go build ./..., go test ./pkg/agent/... ./internal/daemon/... ./internal/handler/..., pnpm typecheck and pnpm test (138 passed). Co-authored-by: Copilot <[email protected]> * refactor(agent): address code review feedback on Kimi runtime - Share ACP provider-error sniffer between hermes and kimi. Previously only hermes promoted stderr-observed 4xx/5xx into a failed task; kimi would report "completed + empty output" when the Moonshot upstream rejected a request (expired token, rate limit, …). Rename hermesProviderErrorSniffer → acpProviderErrorSniffer and parameterise the provider name; wire it into kimiBackend.Execute the same way. - Rename extractHermesSessionID → extractACPSessionID (shared by all ACP backends) so the name matches parseACPSessionNewModels. - Drop the redundant second argument to kimiToolNameFromTitle; the Message struct has only one relevant field (Tool), so passing it twice was a dead fallback. Document that the function normalises residual capitalised kimi titles not caught by hermesToolNameFromTitle. - Remove kimi-only cmd.WaitDelay override; the hermes baseline is fine for both and divergence adds noise. - Add TestKimiBackendSetModelFailureFailsTask: fake `kimi acp` binary that returns a JSON-RPC error for session/set_model, asserts that the task result surfaces status=failed with the model name + upstream message and preserves the session id. - Fix stale agent listings in agent.go / daemon/config.go doc comments (missing cursor, gemini, copilot). All: `go build ./...`, `go vet ./...`, `go test ./pkg/agent/... ./internal/daemon/... ./internal/handler/...` green. * fix(agent/kimi): pass --yolo so Shell tools don't hang on approval Kimi's default config has `default_yolo = false`. Every Shell/file-mutating tool call causes kimi acp to send a `session/request_permission` request and block (up to 300s) waiting for a response. The daemon's hermesClient only handles `session/update` notifications — permission requests go unanswered, the tool call times out, and the UI loop eventually dies ("UI loop timed out"). Observed with the first real kimi task: agent sat as Live for ~7 minutes before the daemon killed it. The fix mirrors hermes' HERMES_YOLO_MODE=1 override: pass `--yolo` to `kimi` so it auto-approves everything. `--yolo` is a top-level flag on the `kimi` CLI (not a flag on `kimi acp`), so it must come before the `acp` subcommand in argv. Added to kimiBlockedArgs so user custom_args can't strip it. While here, fix a related bug that made kimi tool names show up empty in the daemon log ("tool #1: "): hermesToolNameFromTitle's fallback returned `kind` when neither title-with-colon nor kind matched a known tool. Kimi's ACP `tool_call` emits bare titles like "Shell" or "Read file" with no `kind` at all, so we'd drop the title on the floor before kimiToolNameFromTitle ever got a chance to map it. Now: preserve the title when kind is unclassified; hermes titles always carry a colon so this branch never fires for hermes. Tests: - TestKimiBackendPassesYoloFlag — fake binary that records its argv, asserts --yolo comes before acp. - TestHermesToolNameFromTitle rows for bare kimi-style titles. - Existing suite green: go build, go vet, full pkg/agent + daemon + handler test packages. * fix(agent/acp): auto-approve session/request_permission from agent The previous attempt (`kimi --yolo acp`) was a no-op. Inspected the kimi-cli source: the `acp` Typer subcommand takes no parameters, so flags on the root `kimi` command are dropped before `acp_main()` runs — it's impossible to opt into YOLO mode through CLI flags for ACP. The real fix is on our side: respond to session/request_permission. ACP is bidirectional. When kimi runs a Shell or file-write tool, it sends `session/request_permission` (agent → client, JSON-RPC request with id + method) and waits up to 300s for a response. Our existing hermesClient.handleLine only dispatched: (id + result/error) → handleResponse, and (no id + method) → handleNotification. A request with BOTH id and method fell through and got silently dropped — kimi timed out, UI loop died, task sat stuck for 7 minutes. Add handleAgentRequest: for session/request_permission, echo the id and respond with outcome=selected, optionId=approve_for_session. The daemon is headless; there's no user to prompt. `approve_for_session` lets the agent remember the action so subsequent identical calls (every Shell, every file write) skip the round-trip entirely. For any other agent → client method, reply with standard -32601 method-not- found so the agent doesn't block. Also: - Add writeMu so request() (main goroutine) and handleAgentRequest (reader goroutine) don't interleave JSON frames on stdin. - Revert the `--yolo acp` flag — it's a no-op, and carrying it in kimiBlockedArgs gives the wrong impression that it does something. Comment in kimi.go now points at handleAgentRequest as the real fix. Tests: - TestHermesClientAutoApprovesPermissionRequest: inject a session/request_permission, assert the reply echoes the id and carries {outcome: selected, optionId: approve_for_session}. - TestHermesClientReplesMethodNotFoundForUnknownAgentRequest: confirm unknown agent → client methods get JSON-RPC -32601 instead of silence. - TestKimiBackendInvokesACPSubcommand replaces the yolo-flag assertion with a negative assertion: no dead --yolo / --auto-approve / -y on argv, since they'd pretend to do something they can't. All: go build ./..., go vet ./..., go test ./pkg/agent/... green. * fix(agent/acp): surface kimi tool input/output via content blocks Kimi-cli emits tool_call and tool_call_update ACP frames with the input/output inside a `content` array of ContentToolCallContent blocks (shape: {type:"content", content:{type:"text", text:"..."}}), not in the hermes-style `rawInput` map / `rawOutput` string. Our parser only looked at rawInput/rawOutput, so the daemon recorded empty Input and Output for every kimi tool — the execution-history UI showed blank terminal panels even for commands that ran fine. Add extractACPToolCallText() and a fallback in handleToolCallStart / handleToolCallUpdate: when rawInput is nil / rawOutput is empty, pull the text out of the content blocks. rawInput / rawOutput still take precedence so hermes' behaviour is untouched. Terminal / FileEditToolCallContent blocks are skipped (we have nothing to render them as — kimi only emits TerminalToolCallContent when the client advertises terminal capability, which we don't). Tests: - TestHermesClientHandleToolCallStartKimiContent — content array → Input.text populated. - TestHermesClientHandleToolCallCompleteKimiContent — multi-block content → Output concatenated with newline separator. - TestHermesClientHandleToolCallRawOutputTakesPrecedence — hermes rawOutput still wins when both are present. - TestExtractACPToolCallText — unit coverage for the helper (single/multiple text blocks, terminal-block skip, empty input). * fix(agent/acp): buffer streaming tool args so Input isn't empty in UI kimi-cli streams tool args token-by-token via tool_call_update frames — the initial tool_call carries an empty content block and each subsequent in_progress update carries the cumulative JSON so far (`{`, `{"comma`, `{"command": "echo`, …). The final completed update then carries the tool's stdout, not the args. Observed per kimi-cli acp/session.py::_send_tool_call{,_part,_result} and confirmed by driving a real Shell call end-to-end: 10 in_progress frames, last with `{"command": "echo hello world"}`, then completed with `hello world\n`. Our previous handleToolCallStart emitted MessageToolUse on the first tool_call frame, capturing the empty content — so every kimi tool appeared in the execution-history UI with a blank input. Output was correct (fix 4335c198) but command was missing. Changes: - hermesClient now tracks pending tool calls per toolCallId. Hermes path is unchanged — rawInput is present at tool_call time, so emit-immediately-then-flag-emitted still fires on the initial frame. - kimi path defers MessageToolUse until status=completed / failed. tool_call_update in_progress frames update the buffered argsText (cumulative, so overwrite); on completion we parse the accumulated JSON into Message.Input. Malformed JSON falls back to `{"text": …}` so non-JSON tool args still render. - Orphan completion frames (no matching tool_call seen — e.g. daemon restarted mid-task) synthesise ToolUse from the update's own title/kind/rawInput so the UI still gets a header. - extractACPToolCallText now also renders FileEditToolCallContent blocks as a compact header ("--- path / +++ path / (edited: N → M bytes)"). kimi emits these for Write / StrReplaceFile / Patch when the tool's display block is a DiffDisplayBlock. Tests: - TestHermesClientKimiStreamingToolCall: empty tool_call + 5 streaming in_progress + completed. Asserts no emission until complete, then [ToolUse(Input.command="echo hi"), ToolResult(Output="hi\n")]. - TestHermesClientKimiMalformedArgsFallback: non-JSON argsText → falls back to Input.text. - TestHermesClientHandleToolCallCompleteOrphan: completed frame without a start → ToolUse synthesised from update's rawInput. - TestExtractACPToolCallText: diff + new-file-diff cases. All agent / daemon / handler test packages green. --------- Co-authored-by: Eve <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: Eve <[email protected]> Co-authored-by: Lambda <[email protected]>
202 lines
14 KiB
Go
202 lines
14 KiB
Go
package execenv
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// InjectRuntimeConfig writes the meta skill content into the runtime-specific
|
|
// config file so the agent discovers its environment through its native mechanism.
|
|
//
|
|
// For Claude: writes {workDir}/CLAUDE.md (skills discovered natively from .claude/skills/)
|
|
// For Codex: writes {workDir}/AGENTS.md (skills discovered natively via CODEX_HOME)
|
|
// For Copilot: writes {workDir}/AGENTS.md (skills discovered natively from .github/skills/)
|
|
// For OpenCode: writes {workDir}/AGENTS.md (skills discovered natively from .config/opencode/skills/)
|
|
// For OpenClaw: writes {workDir}/AGENTS.md (skills discovered natively from .openclaw/skills/)
|
|
// For Gemini: writes {workDir}/GEMINI.md (discovered natively by the Gemini CLI)
|
|
// For Pi: writes {workDir}/AGENTS.md (skills discovered natively from ~/.pi/agent/skills/)
|
|
// For Cursor: writes {workDir}/AGENTS.md (skills discovered natively from .cursor/skills/)
|
|
// For Kimi: writes {workDir}/AGENTS.md (Kimi Code CLI reads AGENTS.md natively; skills auto-discovered from project skills dirs)
|
|
func InjectRuntimeConfig(workDir, provider string, ctx TaskContextForEnv) error {
|
|
content := buildMetaSkillContent(provider, ctx)
|
|
|
|
switch provider {
|
|
case "claude":
|
|
return os.WriteFile(filepath.Join(workDir, "CLAUDE.md"), []byte(content), 0o644)
|
|
case "codex", "copilot", "opencode", "openclaw", "pi", "cursor", "kimi":
|
|
return os.WriteFile(filepath.Join(workDir, "AGENTS.md"), []byte(content), 0o644)
|
|
case "gemini":
|
|
return os.WriteFile(filepath.Join(workDir, "GEMINI.md"), []byte(content), 0o644)
|
|
default:
|
|
// Unknown provider — skip config injection, prompt-only mode.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// buildMetaSkillContent generates the meta skill markdown that teaches the agent
|
|
// about the Multica runtime environment and available CLI tools.
|
|
func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString("# Multica Agent Runtime\n\n")
|
|
b.WriteString("You are a coding agent in the Multica platform. Use the `multica` CLI to interact with the platform.\n\n")
|
|
|
|
// Always emit agent identity so the agent knows who it is, even when
|
|
// dispatched via @mention on an issue assigned to a different agent.
|
|
if ctx.AgentName != "" || ctx.AgentID != "" {
|
|
b.WriteString("## Agent Identity\n\n")
|
|
if ctx.AgentName != "" {
|
|
fmt.Fprintf(&b, "**You are: %s**", ctx.AgentName)
|
|
if ctx.AgentID != "" {
|
|
fmt.Fprintf(&b, " (ID: `%s`)", ctx.AgentID)
|
|
}
|
|
b.WriteString("\n\n")
|
|
}
|
|
if ctx.AgentInstructions != "" {
|
|
b.WriteString(ctx.AgentInstructions)
|
|
b.WriteString("\n\n")
|
|
}
|
|
} else if ctx.AgentInstructions != "" {
|
|
b.WriteString("## Agent Identity\n\n")
|
|
b.WriteString(ctx.AgentInstructions)
|
|
b.WriteString("\n\n")
|
|
}
|
|
|
|
b.WriteString("## Available Commands\n\n")
|
|
b.WriteString("**Always use `--output json` for all read commands** to get structured data with full IDs.\n\n")
|
|
b.WriteString("### Read\n")
|
|
b.WriteString("- `multica issue get <id> --output json` — Get full issue details (title, description, status, priority, assignee)\n")
|
|
b.WriteString("- `multica issue list [--status X] [--priority X] [--assignee X] [--limit N] [--offset N] --output json` — List issues in workspace (default limit: 50; JSON output includes `total`, `has_more` — use offset to paginate when `has_more` is true)\n")
|
|
b.WriteString("- `multica issue comment list <issue-id> [--limit N] [--offset N] [--since <RFC3339>] --output json` — List comments on an issue (supports pagination; includes id, parent_id for threading)\n")
|
|
b.WriteString("- `multica workspace get --output json` — Get workspace details and context\n")
|
|
b.WriteString("- `multica workspace members [workspace-id] --output json` — List workspace members (user IDs, names, roles)\n")
|
|
b.WriteString("- `multica agent list --output json` — List agents in workspace\n")
|
|
b.WriteString("- `multica repo checkout <url>` — Check out a repository into the working directory (creates a git worktree with a dedicated branch)\n")
|
|
b.WriteString("- `multica issue runs <issue-id> --output json` — List all execution runs for an issue (status, timestamps, errors)\n")
|
|
b.WriteString("- `multica issue run-messages <task-id> [--since <seq>] --output json` — List messages for a specific execution run (supports incremental fetch)\n")
|
|
b.WriteString("- `multica attachment download <id> [-o <dir>]` — Download an attachment file locally by ID\n")
|
|
b.WriteString("- `multica autopilot list [--status X] --output json` — List autopilots (scheduled/triggered agent automations) in the workspace\n")
|
|
b.WriteString("- `multica autopilot get <id> --output json` — Get autopilot details including triggers\n")
|
|
b.WriteString("- `multica autopilot runs <id> [--limit N] --output json` — List execution history for an autopilot\n\n")
|
|
|
|
b.WriteString("### Write\n")
|
|
b.WriteString("- `multica issue create --title \"...\" [--description \"...\"] [--priority X] [--assignee X] [--parent <issue-id>] [--status X]` — Create a new issue\n")
|
|
b.WriteString("- `multica issue assign <id> --to <name>` — Assign an issue to a member or agent by name (use --unassign to remove assignee)\n")
|
|
b.WriteString("- `multica issue comment add <issue-id> --content \"...\" [--parent <comment-id>]` — Post a comment (use --parent to reply to a specific comment)\n")
|
|
b.WriteString(" - For content with special characters (backticks, quotes), pipe via stdin: `cat <<'COMMENT' | multica issue comment add <issue-id> --content-stdin`\n")
|
|
b.WriteString("- `multica issue comment delete <comment-id>` — Delete a comment\n")
|
|
b.WriteString("- `multica issue status <id> <status>` — Update issue status (todo, in_progress, in_review, done, blocked)\n")
|
|
b.WriteString("- `multica issue update <id> [--title X] [--description X] [--priority X]` — Update issue fields\n")
|
|
b.WriteString("- `multica autopilot create --title \"...\" --agent <name> --mode create_issue [--description \"...\"]` — Create an autopilot\n")
|
|
b.WriteString("- `multica autopilot update <id> [--title X] [--description X] [--status active|paused]` — Update an autopilot\n")
|
|
b.WriteString("- `multica autopilot trigger <id>` — Manually trigger an autopilot to run once\n")
|
|
b.WriteString("- `multica autopilot delete <id>` — Delete an autopilot\n\n")
|
|
|
|
// Inject available repositories section.
|
|
if len(ctx.Repos) > 0 {
|
|
b.WriteString("## Repositories\n\n")
|
|
b.WriteString("The following code repositories are available in this workspace.\n")
|
|
b.WriteString("Use `multica repo checkout <url>` to check out a repository into your working directory.\n\n")
|
|
b.WriteString("| URL | Description |\n")
|
|
b.WriteString("|-----|-------------|\n")
|
|
for _, repo := range ctx.Repos {
|
|
desc := repo.Description
|
|
if desc == "" {
|
|
desc = "—"
|
|
}
|
|
fmt.Fprintf(&b, "| %s | %s |\n", repo.URL, desc)
|
|
}
|
|
b.WriteString("\nThe checkout command creates a git worktree with a dedicated branch. You can check out one or more repos as needed.\n\n")
|
|
}
|
|
|
|
b.WriteString("### Workflow\n\n")
|
|
|
|
if ctx.ChatSessionID != "" {
|
|
// Chat task: interactive assistant mode
|
|
b.WriteString("**You are in chat mode.** A user is messaging you directly in a chat window.\n\n")
|
|
b.WriteString("- Respond conversationally and helpfully to the user's message\n")
|
|
b.WriteString("- You have full access to the `multica` CLI to look up issues, workspace info, members, agents, etc.\n")
|
|
b.WriteString("- If asked about issues, use `multica issue list --output json` or `multica issue get <id> --output json`\n")
|
|
b.WriteString("- If asked about the workspace, use `multica workspace get --output json`\n")
|
|
b.WriteString("- If asked to perform actions (create issues, update status, etc.), use the appropriate CLI commands\n")
|
|
b.WriteString("- If the task requires code changes, use `multica repo checkout <url>` to get the code first\n")
|
|
b.WriteString("- Keep responses concise and direct\n\n")
|
|
} else if ctx.TriggerCommentID != "" {
|
|
// Comment-triggered: focus on reading and replying
|
|
b.WriteString("**This task was triggered by a NEW comment.** Your primary job is to respond to THIS specific comment, even if you have handled similar requests before in this session.\n\n")
|
|
fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand the issue context\n", ctx.IssueID)
|
|
fmt.Fprintf(&b, "2. Run `multica issue comment list %s --output json` to read the conversation\n", ctx.IssueID)
|
|
b.WriteString(" - If the output is very large or truncated, use pagination: `--limit 30` to get the latest 30 comments, or `--since <timestamp>` to fetch only recent ones\n")
|
|
fmt.Fprintf(&b, "3. Find the triggering comment (ID: `%s`) and understand what is being asked — do NOT confuse it with previous comments\n", ctx.TriggerCommentID)
|
|
b.WriteString("4. If the comment requests code changes or further work, do the work first\n")
|
|
b.WriteString("5. **Post your reply as a comment — this step is mandatory.** Text in your terminal or run logs is NOT delivered to the user. ")
|
|
b.WriteString(BuildCommentReplyInstructions(ctx.IssueID, ctx.TriggerCommentID))
|
|
b.WriteString("6. Do NOT change the issue status unless the comment explicitly asks for it\n\n")
|
|
} else {
|
|
// Assignment-triggered: defer to agent Skills for workflow specifics.
|
|
b.WriteString("You are responsible for managing the issue status throughout your work.\n\n")
|
|
fmt.Fprintf(&b, "1. Run `multica issue get %s --output json` to understand your task\n", ctx.IssueID)
|
|
fmt.Fprintf(&b, "2. Run `multica issue status %s in_progress`\n", ctx.IssueID)
|
|
b.WriteString("3. Read comments for additional context or human instructions\n")
|
|
b.WriteString("4. Follow your Skills and Agent Identity to complete the task (write code, investigate, etc.)\n")
|
|
fmt.Fprintf(&b, "5. **Post your final results as a comment — this step is mandatory**: `multica issue comment add %s --content \"...\"`. Your results are only visible to the user if posted via this CLI call; text in your terminal or run logs is NOT delivered.\n", ctx.IssueID)
|
|
fmt.Fprintf(&b, "6. When done, run `multica issue status %s in_review`\n", ctx.IssueID)
|
|
fmt.Fprintf(&b, "7. If blocked, run `multica issue status %s blocked` and post a comment explaining why\n\n", ctx.IssueID)
|
|
}
|
|
|
|
if len(ctx.AgentSkills) > 0 {
|
|
b.WriteString("## Skills\n\n")
|
|
switch provider {
|
|
case "claude":
|
|
// Claude discovers skills natively from .claude/skills/ — just list names.
|
|
b.WriteString("You have the following skills installed (discovered automatically):\n\n")
|
|
case "codex", "copilot", "opencode", "openclaw", "pi", "cursor", "kimi":
|
|
// Codex, Copilot, OpenCode, OpenClaw, Pi, Cursor, and Kimi discover skills natively from their respective paths — just list names.
|
|
b.WriteString("You have the following skills installed (discovered automatically):\n\n")
|
|
case "gemini":
|
|
// Gemini reads GEMINI.md directly; point it at the fallback skills dir.
|
|
b.WriteString("Detailed skill instructions are in `.agent_context/skills/`. Each subdirectory contains a `SKILL.md`.\n\n")
|
|
default:
|
|
b.WriteString("Detailed skill instructions are in `.agent_context/skills/`. Each subdirectory contains a `SKILL.md`.\n\n")
|
|
}
|
|
for _, skill := range ctx.AgentSkills {
|
|
fmt.Fprintf(&b, "- **%s**\n", skill.Name)
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
b.WriteString("## Mentions\n\n")
|
|
b.WriteString("When referencing issues or people in comments, use the mention format so they render as interactive links:\n\n")
|
|
b.WriteString("- **Issue**: `[MUL-123](mention://issue/<issue-id>)` — renders as a clickable link to the issue\n")
|
|
b.WriteString("- **Member**: `[@Name](mention://member/<user-id>)` — renders as a styled mention and sends a notification\n")
|
|
b.WriteString("- **Agent**: `[@Name](mention://agent/<agent-id>)` — renders as a styled mention and re-triggers the agent\n\n")
|
|
b.WriteString("⚠️ Agent and member mentions are **actions**, not text references: agent mentions enqueue a new task for the agent, and member mentions send a notification. ")
|
|
b.WriteString("If you only want to refer to someone by name in prose (e.g. \"GPT-Boy is correct\"), write the plain name without the mention link.\n\n")
|
|
b.WriteString("Use `multica issue list --output json` to look up issue IDs, and `multica workspace members --output json` for member IDs.\n\n")
|
|
|
|
b.WriteString("## Attachments\n\n")
|
|
b.WriteString("Issues and comments may include file attachments (images, documents, etc.).\n")
|
|
b.WriteString("Use the download command to fetch attachment files locally:\n\n")
|
|
b.WriteString("```\nmultica attachment download <attachment-id>\n```\n\n")
|
|
b.WriteString("This downloads the file to the current directory and prints the local path. Use `-o <dir>` to save elsewhere.\n")
|
|
b.WriteString("After downloading, you can read the file directly (e.g. view an image, read a document).\n\n")
|
|
|
|
b.WriteString("## Important: Always Use the `multica` CLI\n\n")
|
|
b.WriteString("All interactions with Multica platform resources — including issues, comments, attachments, images, files, and any other platform data — **must** go through the `multica` CLI. ")
|
|
b.WriteString("Do NOT use `curl`, `wget`, or any other HTTP client to access Multica URLs or APIs directly. ")
|
|
b.WriteString("Multica resource URLs require authenticated access that only the `multica` CLI can provide.\n\n")
|
|
b.WriteString("If you need to perform an operation that is not covered by any existing `multica` command, ")
|
|
b.WriteString("do NOT attempt to work around it. Instead, post a comment mentioning the workspace owner to request the missing functionality.\n\n")
|
|
|
|
b.WriteString("## Output\n\n")
|
|
b.WriteString("⚠️ **Final results MUST be delivered via `multica issue comment add`.** The user does NOT see your terminal output, assistant chat text, or run logs — only comments on the issue. A task that finishes without a result comment is invisible to the user, even if the work itself was correct.\n\n")
|
|
b.WriteString("Keep comments concise and natural — state the outcome, not the process.\n")
|
|
b.WriteString("Good: \"Fixed the login redirect. PR: https://...\"\n")
|
|
b.WriteString("Bad: \"1. Read the issue 2. Found the bug in auth.go 3. Created branch 4. ...\"\n")
|
|
b.WriteString("When referencing issues in comments, **always** use the mention format `[MUL-123](mention://issue/<issue-id>)` so they render as clickable links.\n")
|
|
|
|
return b.String()
|
|
}
|