mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-20 13:18:56 +02:00
Compare commits
1 Commits
agent/lamb
...
agent/lamb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6ee718117 |
@@ -33,16 +33,20 @@ func writeContextFiles(workDir, provider string, ctx TaskContextForEnv) error {
|
||||
return fmt.Errorf("write issue_context.md: %w", err)
|
||||
}
|
||||
|
||||
if len(ctx.AgentSkills) > 0 {
|
||||
// Codex skills are written to codex-home in Prepare; skip writing them
|
||||
// into the workdir's skills dir here. The built-in multica-cli skill is
|
||||
// always installed (regardless of whether the agent has user-defined
|
||||
// skills) so the agent can lazy-load the full CLI manual on demand —
|
||||
// see MUL-1821 for why the manual is no longer inlined into the runtime
|
||||
// config.
|
||||
if provider != "codex" {
|
||||
skillsDir, err := resolveSkillsDir(workDir, provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve skills dir: %w", err)
|
||||
}
|
||||
// Codex skills are written to codex-home in Prepare; skip here.
|
||||
if provider != "codex" {
|
||||
if err := writeSkillFiles(skillsDir, ctx.AgentSkills); err != nil {
|
||||
return fmt.Errorf("write skill files: %w", err)
|
||||
}
|
||||
skills := append([]SkillContextForEnv{builtinMulticaCLISkill()}, ctx.AgentSkills...)
|
||||
if err := writeSkillFiles(skillsDir, skills); err != nil {
|
||||
return fmt.Errorf("write skill files: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,10 +197,12 @@ func Reuse(workDir, provider, codexVersion string, task TaskContextForEnv, logge
|
||||
}
|
||||
|
||||
func writeCodexWorkspaceSkills(codexHome string, skills []SkillContextForEnv) error {
|
||||
if len(skills) == 0 {
|
||||
return nil
|
||||
}
|
||||
return writeSkillFiles(filepath.Join(codexHome, "skills"), skills)
|
||||
// The built-in multica-cli skill is always installed alongside the
|
||||
// agent's user-defined skills so the lazy-loaded full CLI manual is
|
||||
// available even when the agent has no other skills attached
|
||||
// (MUL-1821).
|
||||
all := append([]SkillContextForEnv{builtinMulticaCLISkill()}, skills...)
|
||||
return writeSkillFiles(filepath.Join(codexHome, "skills"), all)
|
||||
}
|
||||
|
||||
// GCMeta is persisted to .gc_meta.json inside the env root so the GC loop
|
||||
|
||||
@@ -669,8 +669,13 @@ func TestInjectRuntimeConfigNoSkills(t *testing.T) {
|
||||
if !strings.Contains(s, "multica issue get") {
|
||||
t.Error("should reference multica CLI even without skills")
|
||||
}
|
||||
if strings.Contains(s, "## Skills") {
|
||||
t.Error("should not have Skills section when there are no skills")
|
||||
// The built-in multica-cli skill is always installed (MUL-1821) so the
|
||||
// Skills section is always rendered even without user-defined skills.
|
||||
if !strings.Contains(s, "## Skills") {
|
||||
t.Error("expected Skills section listing the built-in multica-cli skill")
|
||||
}
|
||||
if !strings.Contains(s, multicaCLISkillName) {
|
||||
t.Errorf("Skills section missing built-in skill %q", multicaCLISkillName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -894,14 +899,16 @@ func TestPrepareWithRepoContextOpencode(t *testing.T) {
|
||||
t.Fatalf("InjectRuntimeConfig failed: %v", err)
|
||||
}
|
||||
|
||||
// Workdir should only contain expected entries.
|
||||
// Workdir should only contain expected entries. `.opencode` is created
|
||||
// because the built-in multica-cli skill is installed natively under
|
||||
// `.opencode/skills/` (MUL-1821).
|
||||
entries, err := os.ReadDir(env.WorkDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read workdir: %v", err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if name != ".agent_context" && name != "AGENTS.md" {
|
||||
if name != ".agent_context" && name != "AGENTS.md" && name != ".opencode" {
|
||||
t.Errorf("unexpected entry in workdir: %s", name)
|
||||
}
|
||||
}
|
||||
@@ -2086,213 +2093,3 @@ func TestInjectRuntimeConfigMentionLoopHardening(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// firstDynamicSectionOffset returns the offset of the first per-issue dynamic
|
||||
// header in CLAUDE.md content. The stable prefix is everything before this
|
||||
// offset; everything from this offset on may vary per issue (Repositories
|
||||
// because of project repo override, Project Context because some issues have a
|
||||
// project, Workflow because it carries the issue ID).
|
||||
func firstDynamicSectionOffset(t *testing.T, s string) int {
|
||||
t.Helper()
|
||||
min := -1
|
||||
for _, marker := range []string{"## Repositories", "## Project Context", "### Workflow"} {
|
||||
pos := strings.Index(s, marker)
|
||||
if pos < 0 {
|
||||
continue
|
||||
}
|
||||
if min < 0 || pos < min {
|
||||
min = pos
|
||||
}
|
||||
}
|
||||
if min < 0 {
|
||||
t.Fatalf("no dynamic section found (Repositories / Project Context / Workflow)\n---\n%s", s)
|
||||
}
|
||||
return min
|
||||
}
|
||||
|
||||
// TestInjectRuntimeConfigStablePrefixOrdering pins the prompt-cache-friendly
|
||||
// section ordering: every section that is stable across issues for the same
|
||||
// agent must appear before the first per-issue dynamic section. Without this
|
||||
// ordering the prompt prefix cache misses on every issue switch because a
|
||||
// dynamic block is buried in the middle of the prompt. See MUL-1824.
|
||||
func TestInjectRuntimeConfigStablePrefixOrdering(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
ctx := TaskContextForEnv{
|
||||
AgentName: "Lambda",
|
||||
AgentID: "agent-uuid",
|
||||
IssueID: "ISSUE-UUID-MARKER",
|
||||
AgentSkills: []SkillContextForEnv{
|
||||
{Name: "Coding", Content: "Write good code."},
|
||||
},
|
||||
Repos: []RepoContextForEnv{
|
||||
{URL: "https://github.com/org/repo"},
|
||||
},
|
||||
ProjectID: "project-uuid",
|
||||
ProjectTitle: "Project A",
|
||||
}
|
||||
if err := InjectRuntimeConfig(dir, "claude", ctx); err != nil {
|
||||
t.Fatalf("InjectRuntimeConfig: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(dir, "CLAUDE.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read CLAUDE.md: %v", err)
|
||||
}
|
||||
s := string(data)
|
||||
|
||||
dynamicStart := firstDynamicSectionOffset(t, s)
|
||||
|
||||
workflowPos := strings.Index(s, "### Workflow")
|
||||
if workflowPos < 0 {
|
||||
t.Fatalf("Workflow section missing")
|
||||
}
|
||||
issueIDPos := strings.Index(s, "ISSUE-UUID-MARKER")
|
||||
if issueIDPos < 0 {
|
||||
t.Fatalf("issue ID marker not found in CLAUDE.md")
|
||||
}
|
||||
if issueIDPos < workflowPos {
|
||||
t.Fatalf("issue ID appears before Workflow header — Workflow must be the only section that contains the issue ID")
|
||||
}
|
||||
|
||||
// Stable prefix sections must come before the first dynamic section.
|
||||
stablePrefix := []string{
|
||||
"## Available Commands",
|
||||
"## Skills",
|
||||
"## Mentions",
|
||||
"## Attachments",
|
||||
"## Important: Always Use the `multica` CLI",
|
||||
"## Output",
|
||||
}
|
||||
for _, section := range stablePrefix {
|
||||
pos := strings.Index(s, section)
|
||||
if pos < 0 {
|
||||
t.Errorf("missing section %q", section)
|
||||
continue
|
||||
}
|
||||
if pos > dynamicStart {
|
||||
t.Errorf("stable section %q at offset %d appears after the first dynamic section at offset %d — must come before per-issue content for prompt-cache friendliness", section, pos, dynamicStart)
|
||||
}
|
||||
}
|
||||
|
||||
// Within the dynamic suffix the order must be Repositories → Project
|
||||
// Context → Workflow so Workflow stays the very last section.
|
||||
reposPos := strings.Index(s, "## Repositories")
|
||||
projectPos := strings.Index(s, "## Project Context")
|
||||
if reposPos < 0 {
|
||||
t.Errorf("missing Repositories section")
|
||||
}
|
||||
if projectPos < 0 {
|
||||
t.Errorf("missing Project Context section")
|
||||
}
|
||||
if reposPos > projectPos {
|
||||
t.Errorf("Repositories (%d) must precede Project Context (%d)", reposPos, projectPos)
|
||||
}
|
||||
if projectPos > workflowPos {
|
||||
t.Errorf("Project Context (%d) must precede Workflow (%d)", projectPos, workflowPos)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInjectRuntimeConfigStablePrefixIsByteAligned validates the core
|
||||
// prompt-cache property: for the same agent, the bytes up to the first
|
||||
// dynamic section header are byte-identical regardless of which issue (or
|
||||
// which project repos, or which project) is rendered. If this regresses,
|
||||
// prompt-prefix caches on the provider side miss on every issue switch.
|
||||
func TestInjectRuntimeConfigStablePrefixIsByteAligned(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
render := func(t *testing.T, ctx TaskContextForEnv) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
if err := InjectRuntimeConfig(dir, "claude", ctx); err != nil {
|
||||
t.Fatalf("InjectRuntimeConfig: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(dir, "CLAUDE.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read CLAUDE.md: %v", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
baseCtx := func(issueID string) TaskContextForEnv {
|
||||
return TaskContextForEnv{
|
||||
AgentName: "Lambda",
|
||||
AgentID: "agent-uuid-1",
|
||||
IssueID: issueID,
|
||||
AgentSkills: []SkillContextForEnv{
|
||||
{Name: "Coding", Content: "Write good code."},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("issue-id-only-changes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := baseCtx("issue-aaaaaaaa")
|
||||
a.Repos = []RepoContextForEnv{{URL: "https://github.com/org/repo"}}
|
||||
b := baseCtx("issue-bbbbbbbb")
|
||||
b.Repos = []RepoContextForEnv{{URL: "https://github.com/org/repo"}}
|
||||
|
||||
sa := render(t, a)
|
||||
sb := render(t, b)
|
||||
|
||||
da := firstDynamicSectionOffset(t, sa)
|
||||
db := firstDynamicSectionOffset(t, sb)
|
||||
if da != db {
|
||||
t.Fatalf("dynamic-section offset differs: %d vs %d", da, db)
|
||||
}
|
||||
if sa[:da] != sb[:db] {
|
||||
t.Errorf("stable prefix differs across issue IDs — prompt prefix cache cannot hit")
|
||||
}
|
||||
if sa == sb {
|
||||
t.Errorf("CLAUDE.md identical for two different issues — issue ID should appear in dynamic suffix")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("project-repos-and-project-context-change", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// handler/daemon.go overrides task.Repos with the issue's project
|
||||
// github_repo resources, so two issues in the same workspace can
|
||||
// legitimately render different Repositories blocks. The stable
|
||||
// prefix above the dynamic suffix must still be byte-identical.
|
||||
a := baseCtx("issue-aaaaaaaa")
|
||||
a.Repos = []RepoContextForEnv{{URL: "https://github.com/org/project-a-repo"}}
|
||||
a.ProjectID = "project-a"
|
||||
a.ProjectTitle = "Project A"
|
||||
a.ProjectResources = []ProjectResourceForEnv{
|
||||
{ID: "res-a", ResourceType: "github_repo", ResourceRef: []byte(`{"url":"https://github.com/org/project-a-repo"}`)},
|
||||
}
|
||||
|
||||
b := baseCtx("issue-bbbbbbbb")
|
||||
b.Repos = []RepoContextForEnv{{URL: "https://github.com/org/project-b-repo"}}
|
||||
b.ProjectID = "project-b"
|
||||
b.ProjectTitle = "Project B"
|
||||
b.ProjectResources = []ProjectResourceForEnv{
|
||||
{ID: "res-b", ResourceType: "github_repo", ResourceRef: []byte(`{"url":"https://github.com/org/project-b-repo"}`)},
|
||||
}
|
||||
|
||||
sa := render(t, a)
|
||||
sb := render(t, b)
|
||||
|
||||
da := firstDynamicSectionOffset(t, sa)
|
||||
db := firstDynamicSectionOffset(t, sb)
|
||||
if da != db {
|
||||
t.Fatalf("dynamic-section offset differs: %d vs %d", da, db)
|
||||
}
|
||||
if sa[:da] != sb[:db] {
|
||||
t.Errorf("stable prefix differs when projects differ — prompt prefix cache cannot hit")
|
||||
}
|
||||
|
||||
// Sanity: per-issue values must NOT leak into the stable prefix.
|
||||
for _, leaked := range []string{
|
||||
"project-a-repo", "project-b-repo",
|
||||
"Project A", "Project B",
|
||||
"issue-aaaaaaaa", "issue-bbbbbbbb",
|
||||
} {
|
||||
if strings.Contains(sa[:da], leaked) {
|
||||
t.Errorf("per-issue value %q leaked into stable prefix of CLAUDE.md (a)", leaked)
|
||||
}
|
||||
if strings.Contains(sb[:db], leaked) {
|
||||
t.Errorf("per-issue value %q leaked into stable prefix of CLAUDE.md (b)", leaked)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
87
server/internal/daemon/execenv/multica_cli_skill.go
Normal file
87
server/internal/daemon/execenv/multica_cli_skill.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package execenv
|
||||
|
||||
// multicaCLISkillName is the public-facing name of the built-in CLI reference
|
||||
// skill. Both the on-disk directory (sanitized) and the rendered Skills section
|
||||
// in CLAUDE.md / AGENTS.md / GEMINI.md derive from this constant.
|
||||
const multicaCLISkillName = "Multica CLI Reference"
|
||||
|
||||
// builtinMulticaCLISkill returns the SKILL.md bundle for the multica CLI
|
||||
// reference. The bundle is installed into every task's skills directory so the
|
||||
// agent can lazy-load the full flag-level manual on demand instead of paying
|
||||
// the ~1.5k-token cost of inlining it into the runtime config (see MUL-1821).
|
||||
//
|
||||
// The frontmatter `name` matches the sanitized directory; the `description`
|
||||
// is what providers like Claude Code use to decide when to autoload the skill.
|
||||
func builtinMulticaCLISkill() SkillContextForEnv {
|
||||
return SkillContextForEnv{
|
||||
Name: multicaCLISkillName,
|
||||
Content: multicaCLISkillContent,
|
||||
}
|
||||
}
|
||||
|
||||
const multicaCLISkillContent = `---
|
||||
name: multica-cli
|
||||
description: Full reference for the Multica CLI. Load this when you need a flag, subcommand, or behavior detail that is not in the CLAUDE.md / AGENTS.md quick reference — for example autopilot management, label/subscriber writes, run-message inspection, project-resource queries, or any flag combination beyond the common path.
|
||||
---
|
||||
|
||||
# Multica CLI Reference
|
||||
|
||||
This skill is the authoritative reference for the ` + "`multica`" + ` CLI. The runtime config (CLAUDE.md / AGENTS.md / GEMINI.md) only lists the high-frequency subset; everything else is here. Use this skill instead of guessing flags, and prefer ` + "`multica <command> --help`" + ` when you need to verify a flag at runtime.
|
||||
|
||||
**Always use ` + "`--output json`" + ` for all read commands** to get structured data with full IDs.
|
||||
|
||||
## Read
|
||||
|
||||
- ` + "`multica issue get <id> --output json`" + ` — Get full issue details (title, description, status, priority, assignee).
|
||||
- ` + "`multica issue list [--status X] [--priority X] [--assignee X | --assignee-id <uuid>] [--limit N] [--offset N] --output json`" + ` — List issues in workspace. Default limit: 50; JSON output includes ` + "`total`" + ` and ` + "`has_more`" + ` — use ` + "`--offset`" + ` to paginate while ` + "`has_more`" + ` is true. Prefer ` + "`--assignee-id <uuid>`" + ` when scripting from ` + "`multica workspace members --output json`" + ` / ` + "`multica agent list --output json`" + `.
|
||||
- ` + "`multica issue comment list <issue-id> [--limit N] [--offset N] [--since <RFC3339>] --output json`" + ` — List comments on an issue. Supports pagination; includes ` + "`id`" + ` and ` + "`parent_id`" + ` for threading.
|
||||
- ` + "`multica issue label list <issue-id> --output json`" + ` — List labels currently attached to an issue.
|
||||
- ` + "`multica issue subscriber list <issue-id> --output json`" + ` — List members/agents subscribed to an issue.
|
||||
- ` + "`multica label list --output json`" + ` — List all labels defined in the workspace (returns id + name + color).
|
||||
- ` + "`multica workspace get --output json`" + ` — Get workspace details and context.
|
||||
- ` + "`multica workspace members [workspace-id] --output json`" + ` — List workspace members (user IDs, names, roles).
|
||||
- ` + "`multica agent list --output json`" + ` — List agents in workspace.
|
||||
- ` + "`multica repo checkout <url> [--ref <branch-or-sha>]`" + ` — Check out a repository into the working directory. Creates a git worktree with a dedicated branch; use ` + "`--ref`" + ` for review/QA on a specific branch, tag, or commit.
|
||||
- ` + "`multica issue runs <issue-id> --output json`" + ` — List all execution runs for an issue (status, timestamps, errors).
|
||||
- ` + "`multica issue run-messages <task-id> [--since <seq>] --output json`" + ` — List messages for a specific execution run. Supports incremental fetch with ` + "`--since`" + `.
|
||||
- ` + "`multica attachment download <id> [-o <dir>]`" + ` — Download an attachment file locally by ID. Prints the local path; use ` + "`-o`" + ` to save elsewhere.
|
||||
- ` + "`multica autopilot list [--status X] --output json`" + ` — List autopilots (scheduled / triggered agent automations) in the workspace.
|
||||
- ` + "`multica autopilot get <id> --output json`" + ` — Get autopilot details including triggers.
|
||||
- ` + "`multica autopilot runs <id> [--limit N] --output json`" + ` — List execution history for an autopilot.
|
||||
- ` + "`multica project get <id> --output json`" + ` — Get project details. Includes ` + "`resource_count`" + `; the resources themselves live at the sub-collection below.
|
||||
- ` + "`multica project resource list <project-id> --output json`" + ` — List resources (e.g. ` + "`github_repo`" + `) attached to a project. Use this when ` + "`resource_count > 0`" + ` and you need the actual refs.
|
||||
|
||||
## Write
|
||||
|
||||
- ` + "`multica issue create --title \"...\" [--description \"...\" | --description-stdin] [--priority X] [--status X] [--assignee X | --assignee-id <uuid>] [--parent <issue-id>] [--project <project-id>] [--due-date <RFC3339>] [--attachment <path>]`" + ` — Create a new issue. ` + "`--attachment`" + ` may be repeated to upload multiple files. Labels and subscribers are not accepted here; attach them after create with the commands below.
|
||||
- ` + "`multica issue update <id> [--title X] [--description X | --description-stdin] [--priority X] [--status X] [--assignee X | --assignee-id <uuid>] [--parent <issue-id>] [--project <project-id>] [--due-date <RFC3339>]`" + ` — Update one or more issue fields in a single call. Use ` + "`--parent \"\"`" + ` to clear the parent.
|
||||
- ` + "`multica issue status <id> <status>`" + ` — Shortcut for ` + "`issue update --status`" + ` when you only need to flip status (` + "`todo`" + `, ` + "`in_progress`" + `, ` + "`in_review`" + `, ` + "`done`" + `, ` + "`blocked`" + `, ` + "`backlog`" + `, ` + "`cancelled`" + `).
|
||||
- ` + "`multica issue assign <id> --to <name>|--to-id <uuid>`" + ` — Assign an issue to a member or agent. ` + "`--to <name>`" + ` does fuzzy name matching; pass ` + "`--to-id <uuid>`" + ` (mutually exclusive with ` + "`--to`" + `) to assign by canonical UUID, e.g. when names overlap. Use ` + "`--unassign`" + ` to clear the assignee.
|
||||
- ` + "`multica issue label add <issue-id> <label-id>`" + ` — Attach a label to an issue (look up the label id via ` + "`multica label list`" + `).
|
||||
- ` + "`multica issue label remove <issue-id> <label-id>`" + ` — Detach a label from an issue.
|
||||
- ` + "`multica issue subscriber add <issue-id> [--user <name>|--user-id <uuid>]`" + ` — Subscribe a member or agent to issue updates (defaults to the caller when neither flag is set; the two flags are mutually exclusive).
|
||||
- ` + "`multica issue subscriber remove <issue-id> [--user <name>|--user-id <uuid>]`" + ` — Unsubscribe a member or agent.
|
||||
- ` + "`multica issue comment add <issue-id> --content-stdin [--parent <comment-id>] [--attachment <path>]`" + ` — Post a comment. Agent-authored comments should always pipe content via stdin, even for short single-line replies. Use ` + "`--parent`" + ` to reply to a specific comment; ` + "`--attachment`" + ` may be repeated.
|
||||
- ` + "`multica issue comment delete <comment-id>`" + ` — Delete a comment.
|
||||
- ` + "`multica label create --name \"...\" --color \"#hex\"`" + ` — Define a new workspace label. Use this only when the label you need does not exist yet; reuse existing labels via ` + "`multica label list`" + ` first.
|
||||
- ` + "`multica autopilot create --title \"...\" --agent <name> --mode create_issue [--description \"...\"]`" + ` — Create an autopilot.
|
||||
- ` + "`multica autopilot update <id> [--title X] [--description X] [--status active|paused]`" + ` — Update an autopilot.
|
||||
- ` + "`multica autopilot trigger <id>`" + ` — Manually trigger an autopilot to run once.
|
||||
- ` + "`multica autopilot delete <id>`" + ` — Delete an autopilot.
|
||||
|
||||
## Multi-line content rule (MUL-1467)
|
||||
|
||||
For ` + "`multica issue comment add`" + ` and the ` + "`--description`" + ` flag on ` + "`multica issue create`" + ` / ` + "`multica issue update`" + `, you MUST pipe via stdin (` + "`--content-stdin`" + ` / ` + "`--description-stdin`" + `) for any content that contains line breaks, paragraphs, code blocks, backticks, or quotes. Inline ` + "`--content \"...\\n\\n...\"`" + ` does not work because bash does not expand backslash escapes inside double quotes — agents using that form ended up with literal four-character ` + "`\\n`" + ` sequences in stored comments.
|
||||
|
||||
Use a HEREDOC instead:
|
||||
|
||||
` + "```" + `
|
||||
cat <<'COMMENT' | multica issue comment add <issue-id> --content-stdin
|
||||
First paragraph.
|
||||
|
||||
Second paragraph with ` + "`code`" + ` and "quotes".
|
||||
COMMENT
|
||||
` + "```" + `
|
||||
|
||||
The same shape works for ` + "`--description-stdin`" + ` on ` + "`issue create`" + ` / ` + "`issue update`" + `.
|
||||
`
|
||||
@@ -74,23 +74,6 @@ func InjectRuntimeConfig(workDir, provider string, ctx TaskContextForEnv) error
|
||||
|
||||
// buildMetaSkillContent generates the meta skill markdown that teaches the agent
|
||||
// about the Multica runtime environment and available CLI tools.
|
||||
//
|
||||
// Section ordering is tuned for prompt-cache prefix matching. Three variability
|
||||
// classes, all stable classes first:
|
||||
// - Globally / per-agent stable: Header, Agent Identity, Available Commands,
|
||||
// Skills, Codex notes, Mentions, Attachments, Important, Output. These
|
||||
// bytes never depend on the issue.
|
||||
// - Per-issue dynamic suffix: Repositories, Project Context, Workflow.
|
||||
// Repositories is dynamic because handler/daemon.go lifts the issue's
|
||||
// project github_repo resources into task.Repos when the project has any
|
||||
// attached, overriding workspace-level repos (see daemon.go ~line 1013).
|
||||
// Project Context renders only when the issue has a project. Workflow
|
||||
// embeds the issue ID and trigger comment ID.
|
||||
//
|
||||
// The first dynamic header acts as the cache boundary; everything before it
|
||||
// is byte-identical for the same agent regardless of which issue is claimed.
|
||||
// Adding a new section: classify it correctly — never insert per-issue values
|
||||
// into the stable prefix, and never put a stable section after a dynamic one.
|
||||
func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
var b strings.Builder
|
||||
|
||||
@@ -119,79 +102,32 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
}
|
||||
|
||||
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("**Always use `--output json` for read commands.** This is a quick reference for the high-frequency commands. Full flag-level documentation for every `multica` subcommand (autopilots, labels, subscribers, project resources, run-messages, etc.) lives in the `multica-cli` skill — load that skill, or run `multica <command> --help`, when you need a flag or command not listed below.\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 | --assignee-id <uuid>] [--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). Prefer `--assignee-id <uuid>` when scripting from `multica workspace members --output json` / `multica agent list --output json`.\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 issue label list <issue-id> --output json` — List labels currently attached to an issue\n")
|
||||
b.WriteString("- `multica issue subscriber list <issue-id> --output json` — List members/agents subscribed to an issue\n")
|
||||
b.WriteString("- `multica label list --output json` — List all labels defined in the workspace (returns id + name + color)\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> [--ref <branch-or-sha>]` — Check out a repository into the working directory (creates a git worktree with a dedicated branch; use `--ref` for review/QA on a specific branch, tag, or commit)\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")
|
||||
b.WriteString("- `multica project get <id> --output json` — Get project details. Includes `resource_count`; the resources themselves live at the sub-collection below.\n")
|
||||
b.WriteString("- `multica project resource list <project-id> --output json` — List resources (e.g. github_repo) attached to a project. Use this when `resource_count > 0` and you need the actual refs.\n\n")
|
||||
b.WriteString("- `multica issue get <id> --output json` — Issue details (title, description, status, priority, assignee)\n")
|
||||
b.WriteString("- `multica issue list [--status X] [--assignee X | --assignee-id <uuid>] [--limit N] --output json` — List issues (default limit 50; paginate with `--offset` when `has_more`)\n")
|
||||
b.WriteString("- `multica issue comment list <issue-id> [--limit N] [--since <RFC3339>] --output json` — Issue comments (paginate large threads)\n")
|
||||
b.WriteString("- `multica workspace get --output json` / `multica workspace members --output json` / `multica agent list --output json` — Workspace context\n")
|
||||
b.WriteString("- `multica label list --output json` — All workspace labels (id + name + color)\n")
|
||||
b.WriteString("- `multica repo checkout <url> [--ref <branch-or-sha>]` — Check out a repo as a git worktree with a dedicated branch\n")
|
||||
b.WriteString("- `multica attachment download <id> [-o <dir>]` — Download an attachment locally\n\n")
|
||||
|
||||
b.WriteString("### Write\n")
|
||||
b.WriteString("- `multica issue create --title \"...\" [--description \"...\"] [--priority X] [--status X] [--assignee X | --assignee-id <uuid>] [--parent <issue-id>] [--project <project-id>] [--due-date <RFC3339>] [--attachment <path>]` — Create a new issue. `--attachment` may be repeated to upload multiple files; labels and subscribers are not accepted here, attach them after create with the commands below.\n")
|
||||
b.WriteString("- `multica issue update <id> [--title X] [--description X] [--priority X] [--status X] [--assignee X | --assignee-id <uuid>] [--parent <issue-id>] [--project <project-id>] [--due-date <RFC3339>]` — Update one or more issue fields in a single call. Use `--parent \"\"` to clear the parent.\n")
|
||||
b.WriteString("- `multica issue status <id> <status>` — Shortcut for `issue update --status` when you only need to flip status (todo, in_progress, in_review, done, blocked, backlog, cancelled)\n")
|
||||
b.WriteString("- `multica issue assign <id> --to <name>|--to-id <uuid>` — Assign an issue to a member or agent. `--to <name>` does fuzzy name matching; pass `--to-id <uuid>` (mutually exclusive with `--to`) to assign by canonical UUID, e.g. when names overlap. Use `--unassign` to clear the assignee.\n")
|
||||
b.WriteString("- `multica issue label add <issue-id> <label-id>` — Attach a label to an issue (look up the label id via `multica label list`)\n")
|
||||
b.WriteString("- `multica issue label remove <issue-id> <label-id>` — Detach a label from an issue\n")
|
||||
b.WriteString("- `multica issue subscriber add <issue-id> [--user <name>|--user-id <uuid>]` — Subscribe a member or agent to issue updates (defaults to the caller when neither flag is set; the two flags are mutually exclusive)\n")
|
||||
b.WriteString("- `multica issue subscriber remove <issue-id> [--user <name>|--user-id <uuid>]` — Unsubscribe a member or agent\n")
|
||||
b.WriteString("- `multica issue comment add <issue-id> --content-stdin [--parent <comment-id>] [--attachment <path>]` — Post a comment. Agent-authored comments should always pipe content via stdin, even for short single-line replies. Use `--parent` to reply to a specific comment; `--attachment` may be repeated.\n")
|
||||
b.WriteString(" - **For comment content, you MUST pipe via stdin; this is mandatory for multi-line content (anything with line breaks, paragraphs, code blocks, backticks, or quotes).** Do not use inline `--content` and do not write `\\n` escapes. Use a HEREDOC instead:\n")
|
||||
b.WriteString("\n")
|
||||
b.WriteString(" ```\n")
|
||||
b.WriteString(" cat <<'COMMENT' | multica issue comment add <issue-id> --content-stdin\n")
|
||||
b.WriteString(" First paragraph.\n")
|
||||
b.WriteString("\n")
|
||||
b.WriteString(" Second paragraph with `code` and \"quotes\".\n")
|
||||
b.WriteString(" COMMENT\n")
|
||||
b.WriteString(" ```\n")
|
||||
b.WriteString("\n")
|
||||
b.WriteString(" - The same rule applies to `--description` on `multica issue create` and `multica issue update` — use `--description-stdin` and pipe a HEREDOC for any multi-line description; the inline `--description \"...\"` form is for short single-line text only.\n")
|
||||
b.WriteString("- `multica issue comment delete <comment-id>` — Delete a comment\n")
|
||||
b.WriteString("- `multica label create --name \"...\" --color \"#hex\"` — Define a new workspace label (use this only when the label you need does not exist yet; reuse existing labels via `multica label list` first)\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")
|
||||
b.WriteString("- `multica issue status <id> <status>` — Flip status (todo, in_progress, in_review, done, blocked, backlog, cancelled)\n")
|
||||
b.WriteString("- `multica issue assign <id> --to <name>|--to-id <uuid>` — Assign to member/agent (`--unassign` clears)\n")
|
||||
b.WriteString("- `multica issue create --title \"...\" [--description-stdin] [--priority X] [--status X] [--assignee X | --assignee-id <uuid>] [--parent <id>] [--project <id>] [--attachment <path>]` — Create an issue (`--attachment` repeatable)\n")
|
||||
b.WriteString("- `multica issue update <id> [--title X] [--description-stdin] [--priority X] [--status X] [--assignee X] [--parent <id>] [--project <id>]` — Update issue fields (use `--parent \"\"` to clear)\n")
|
||||
b.WriteString("- `multica issue label add|remove <issue-id> <label-id>` — Attach/detach a label (look up id via `multica label list`)\n")
|
||||
b.WriteString("- `multica issue comment add <issue-id> --content-stdin [--parent <comment-id>] [--attachment <path>]` — Post a comment (always pipe via stdin)\n\n")
|
||||
|
||||
// Skills are stable per-agent (same agent always renders the same skill
|
||||
// list). Place them in the stable prefix so the cache hit extends past
|
||||
// the CLI command listing.
|
||||
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", "kiro":
|
||||
// Codex, Copilot, OpenCode, OpenClaw, Pi, Cursor, Kimi, and Kiro discover skills natively from their respective paths — just list names.
|
||||
b.WriteString("You have the following skills installed (discovered automatically):\n\n")
|
||||
case "gemini", "hermes":
|
||||
// Gemini reads GEMINI.md directly; Hermes has no native skills discovery path
|
||||
// wired up in resolveSkillsDir, so both fall back to .agent_context/skills/.
|
||||
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("**multi-line content (CRITICAL — see MUL-1467):** `multica issue comment add` and `--description` on `multica issue create` / `multica issue update` MUST pipe via stdin (`--content-stdin` / `--description-stdin`) with a HEREDOC for any content that has line breaks, paragraphs, code blocks, backticks, or quotes. Inline `--content \"...\"` and literal `\\n` escapes will store the 4-char sequence `\\n` instead of a real newline. This rule applies to agent-authored comments even when the reply is a single line.\n\n")
|
||||
b.WriteString("```\n")
|
||||
b.WriteString("cat <<'COMMENT' | multica issue comment add <issue-id> --content-stdin\n")
|
||||
b.WriteString("First paragraph.\n")
|
||||
b.WriteString("\n")
|
||||
b.WriteString("Second paragraph with `code` and \"quotes\".\n")
|
||||
b.WriteString("COMMENT\n")
|
||||
b.WriteString("```\n\n")
|
||||
|
||||
if provider == "codex" {
|
||||
b.WriteString("## Codex-Specific Comment Formatting\n\n")
|
||||
@@ -200,61 +136,7 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
b.WriteString("Do not compress a multi-paragraph answer into one line and do not rely on `\\n` escapes.\n\n")
|
||||
}
|
||||
|
||||
b.WriteString("## Mentions\n\n")
|
||||
b.WriteString("Mention links are **side-effecting actions**, not just formatting:\n\n")
|
||||
b.WriteString("- `[MUL-123](mention://issue/<issue-id>)` — clickable link to an issue (safe, no side effect)\n")
|
||||
b.WriteString("- `[@Name](mention://member/<user-id>)` — **sends a notification to a human**\n")
|
||||
b.WriteString("- `[@Name](mention://agent/<agent-id>)` — **enqueues a new run for that agent**\n\n")
|
||||
b.WriteString("### When NOT to use a mention link\n\n")
|
||||
b.WriteString("- Referring to someone in prose (e.g. \"GPT-Boy is right\") — write the plain name, no link.\n")
|
||||
b.WriteString("- **Replying to another agent that just spoke to you.** By default, do NOT put a `mention://agent/...` link anywhere in your reply. The platform already shows your comment to everyone on the issue; re-mentioning the other agent will make them run again, and if they reply with a mention back, you will be triggered again. That is a loop and it costs the user money.\n")
|
||||
b.WriteString("- Thanking, acknowledging, wrapping up, or signing off. These are exactly the moments where an accidental `@mention` causes the other agent to reply \"you're welcome\" and restart the loop. If the work is done, **end with no mention at all**.\n\n")
|
||||
b.WriteString("### When a mention IS appropriate\n\n")
|
||||
b.WriteString("- Escalating to a human owner who is not yet involved.\n")
|
||||
b.WriteString("- Delegating a concrete sub-task to another agent for the first time, with a clear request.\n")
|
||||
b.WriteString("- The user explicitly asked you to loop someone in.\n\n")
|
||||
b.WriteString("If you are unsure whether a mention is warranted, **don't mention**. Silence ends conversations; `@` restarts them.\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")
|
||||
switch {
|
||||
case ctx.AutopilotRunID != "":
|
||||
b.WriteString("This is a run-only autopilot task, so there may be no issue comment to post. Your final assistant output is captured automatically as the autopilot run result. Keep it concise and state the outcome.\n\n")
|
||||
case ctx.QuickCreatePrompt != "":
|
||||
b.WriteString("This is a quick-create task. There is NO existing issue to comment on. Your final stdout is captured automatically and the platform writes the user's success/failure inbox notification based on whether `multica issue create` succeeded.\n\n")
|
||||
b.WriteString("- Do NOT call `multica issue comment add` — the issue you just created has no conversation context for this run.\n")
|
||||
b.WriteString("- Print exactly one final line: `Created MUL-<n>: <title>` after a successful `multica issue create`.\n")
|
||||
b.WriteString("- On CLI failure, exit with the CLI error as the only output. The platform translates that into a `quick_create_failed` inbox item carrying the original prompt for the user.\n\n")
|
||||
default:
|
||||
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 an issue in a comment, use the issue mention format `[MUL-123](mention://issue/<issue-id>)` so it renders as a clickable link. (Issue mentions have no side effect; only member/agent mentions do — see the Mentions section above.)\n\n")
|
||||
}
|
||||
|
||||
// === Per-issue dynamic suffix begins here. ===
|
||||
//
|
||||
// Repositories is workspace-scoped most of the time, but
|
||||
// handler/daemon.go overrides task.Repos with the issue's project
|
||||
// github_repo resources when the project has any attached. So the
|
||||
// rendered list can vary per issue. Keep this section in the dynamic
|
||||
// suffix so the stable prefix above is never invalidated by a project
|
||||
// switch.
|
||||
// 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")
|
||||
@@ -267,8 +149,7 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
|
||||
// Inject project-scoped context (resources attached to the issue's project).
|
||||
// The full structured payload is also available at .multica/project/resources.json
|
||||
// so skills can consume it programmatically. Per-issue: kept in the
|
||||
// dynamic suffix so it doesn't break prefix cache for project-less issues.
|
||||
// so skills can consume it programmatically.
|
||||
if ctx.ProjectID != "" || len(ctx.ProjectResources) > 0 {
|
||||
b.WriteString("## Project Context\n\n")
|
||||
if ctx.ProjectTitle != "" {
|
||||
@@ -286,9 +167,6 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Workflow contains the issue ID and other per-task identifiers and is
|
||||
// the most variable section in this file. It must come last so all the
|
||||
// stable content above can hit the prompt prefix cache.
|
||||
b.WriteString("### Workflow\n\n")
|
||||
|
||||
if ctx.ChatSessionID != "" {
|
||||
@@ -367,5 +245,76 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
fmt.Fprintf(&b, "7. If blocked, run `multica issue status %s blocked` and post a comment explaining why\n\n", ctx.IssueID)
|
||||
}
|
||||
|
||||
// The built-in multica-cli skill is always installed alongside any
|
||||
// user-defined agent skills (see MUL-1821), so the Skills section is
|
||||
// always rendered.
|
||||
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", "kiro":
|
||||
// Codex, Copilot, OpenCode, OpenClaw, Pi, Cursor, Kimi, and Kiro discover skills natively from their respective paths — just list names.
|
||||
b.WriteString("You have the following skills installed (discovered automatically):\n\n")
|
||||
case "gemini", "hermes":
|
||||
// Gemini reads GEMINI.md directly; Hermes has no native skills discovery path
|
||||
// wired up in resolveSkillsDir, so both fall back to .agent_context/skills/.
|
||||
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")
|
||||
}
|
||||
fmt.Fprintf(&b, "- **%s** — full reference for the `multica` CLI; load when you need a flag or subcommand not in the quick reference above.\n", multicaCLISkillName)
|
||||
for _, skill := range ctx.AgentSkills {
|
||||
fmt.Fprintf(&b, "- **%s**\n", skill.Name)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
|
||||
b.WriteString("## Mentions\n\n")
|
||||
b.WriteString("Mention links are **side-effecting actions**, not just formatting:\n\n")
|
||||
b.WriteString("- `[MUL-123](mention://issue/<issue-id>)` — clickable link to an issue (safe, no side effect)\n")
|
||||
b.WriteString("- `[@Name](mention://member/<user-id>)` — **sends a notification to a human**\n")
|
||||
b.WriteString("- `[@Name](mention://agent/<agent-id>)` — **enqueues a new run for that agent**\n\n")
|
||||
b.WriteString("### When NOT to use a mention link\n\n")
|
||||
b.WriteString("- Referring to someone in prose (e.g. \"GPT-Boy is right\") — write the plain name, no link.\n")
|
||||
b.WriteString("- **Replying to another agent that just spoke to you.** By default, do NOT put a `mention://agent/...` link anywhere in your reply. The platform already shows your comment to everyone on the issue; re-mentioning the other agent will make them run again, and if they reply with a mention back, you will be triggered again. That is a loop and it costs the user money.\n")
|
||||
b.WriteString("- Thanking, acknowledging, wrapping up, or signing off. These are exactly the moments where an accidental `@mention` causes the other agent to reply \"you're welcome\" and restart the loop. If the work is done, **end with no mention at all**.\n\n")
|
||||
b.WriteString("### When a mention IS appropriate\n\n")
|
||||
b.WriteString("- Escalating to a human owner who is not yet involved.\n")
|
||||
b.WriteString("- Delegating a concrete sub-task to another agent for the first time, with a clear request.\n")
|
||||
b.WriteString("- The user explicitly asked you to loop someone in.\n\n")
|
||||
b.WriteString("If you are unsure whether a mention is warranted, **don't mention**. Silence ends conversations; `@` restarts them.\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")
|
||||
switch {
|
||||
case ctx.AutopilotRunID != "":
|
||||
b.WriteString("This is a run-only autopilot task, so there may be no issue comment to post. Your final assistant output is captured automatically as the autopilot run result. Keep it concise and state the outcome.\n")
|
||||
case ctx.QuickCreatePrompt != "":
|
||||
b.WriteString("This is a quick-create task. There is NO existing issue to comment on. Your final stdout is captured automatically and the platform writes the user's success/failure inbox notification based on whether `multica issue create` succeeded.\n\n")
|
||||
b.WriteString("- Do NOT call `multica issue comment add` — the issue you just created has no conversation context for this run.\n")
|
||||
b.WriteString("- Print exactly one final line: `Created MUL-<n>: <title>` after a successful `multica issue create`.\n")
|
||||
b.WriteString("- On CLI failure, exit with the CLI error as the only output. The platform translates that into a `quick_create_failed` inbox item carrying the original prompt for the user.\n")
|
||||
default:
|
||||
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 an issue in a comment, use the issue mention format `[MUL-123](mention://issue/<issue-id>)` so it renders as a clickable link. (Issue mentions have no side effect; only member/agent mentions do — see the Mentions section above.)\n")
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user