mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-05 21:39:54 +02:00
refactor(daemon): reorder meta-skill sections so per-issue Workflow comes last
Current section order interleaves the per-issue Workflow block (which embeds the issue ID) between the stable CLI command listing and the equally-stable Skills/Mentions/Attachments/Output sections. Any issue switch invalidates the provider's prompt prefix cache from the Workflow onward, throwing away ~2.5k tokens of cached prefix. Move the Workflow to the end and group all stable sections (Available Commands, Skills, Codex-specific notes, Mentions, Attachments, Important, Output, Repositories) before any per-issue content. The same agent processing consecutive issues in the same workspace now produces a byte-identical CLAUDE.md prefix up to the Workflow header. Lock the property in with two new tests: - TestInjectRuntimeConfigStablePrefixOrdering — every stable section's offset is < the Workflow offset. - TestInjectRuntimeConfigPrefixIsByteAlignedAcrossIssues — the bytes before ### Workflow are byte-identical for two different IssueIDs. See MUL-1824. Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -2086,3 +2086,128 @@ func TestInjectRuntimeConfigMentionLoopHardening(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestInjectRuntimeConfigStablePrefixOrdering pins the prompt-cache-friendly
|
||||
// section ordering: every section that is stable across issues for the same
|
||||
// agent must appear before the per-issue Workflow (which embeds the issue ID).
|
||||
// Without this ordering the prompt prefix cache misses on every issue switch
|
||||
// because the dynamic Workflow 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)
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// Full-stable prefix sections (universal or per-agent stable) must come
|
||||
// before any per-issue content.
|
||||
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 > workflowPos {
|
||||
t.Errorf("stable section %q at offset %d appears after Workflow at offset %d — must come before per-issue content for prompt-cache friendliness", section, pos, workflowPos)
|
||||
}
|
||||
}
|
||||
|
||||
// Project Context is per-issue but, when present, must still come before
|
||||
// Workflow (so Workflow stays the very last section).
|
||||
projectPos := strings.Index(s, "## Project Context")
|
||||
if projectPos < 0 {
|
||||
t.Errorf("missing Project Context section")
|
||||
} else if projectPos > workflowPos {
|
||||
t.Errorf("Project Context at %d appears after Workflow at %d", projectPos, workflowPos)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInjectRuntimeConfigPrefixIsByteAlignedAcrossIssues validates the core
|
||||
// prompt-cache property: when the same agent processes two different issues
|
||||
// in the same workspace, the bytes leading up to the dynamic Workflow section
|
||||
// must be byte-identical. If this regresses, prompt-prefix caches on the
|
||||
// provider side will miss on every issue switch.
|
||||
func TestInjectRuntimeConfigPrefixIsByteAlignedAcrossIssues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
build := func(t *testing.T, issueID string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
ctx := TaskContextForEnv{
|
||||
AgentName: "Lambda",
|
||||
AgentID: "agent-uuid-1",
|
||||
IssueID: issueID,
|
||||
AgentSkills: []SkillContextForEnv{
|
||||
{Name: "Coding", Content: "Write good code."},
|
||||
},
|
||||
Repos: []RepoContextForEnv{
|
||||
{URL: "https://github.com/org/repo"},
|
||||
},
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
a := build(t, "issue-aaaaaaaa")
|
||||
b := build(t, "issue-bbbbbbbb")
|
||||
|
||||
aWorkflow := strings.Index(a, "### Workflow")
|
||||
bWorkflow := strings.Index(b, "### Workflow")
|
||||
if aWorkflow < 0 || bWorkflow < 0 {
|
||||
t.Fatalf("Workflow section missing (a=%d b=%d)", aWorkflow, bWorkflow)
|
||||
}
|
||||
if aWorkflow != bWorkflow {
|
||||
t.Fatalf("Workflow at different offsets: %d vs %d — prefix not byte-aligned", aWorkflow, bWorkflow)
|
||||
}
|
||||
if a[:aWorkflow] != b[:bWorkflow] {
|
||||
t.Errorf("CLAUDE.md prefix differs between two issues for the same agent — prompt prefix cache cannot hit")
|
||||
}
|
||||
if a == b {
|
||||
t.Errorf("CLAUDE.md is identical for two different issues — issue ID should appear in Workflow suffix")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,14 @@ 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: globally and
|
||||
// per-agent stable content comes first, per-workspace stable next, per-issue
|
||||
// content (Project Context, Workflow with the issue ID) last. When the same
|
||||
// agent processes consecutive issues in the same workspace the entire prefix
|
||||
// up to "## Repositories" stays byte-identical and hits the provider's prompt
|
||||
// prefix cache. Adding a new section: place it according to its variability
|
||||
// class — never insert per-issue values into the stable prefix.
|
||||
func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
var b strings.Builder
|
||||
|
||||
@@ -151,6 +159,31 @@ func buildMetaSkillContent(provider string, ctx TaskContextForEnv) string {
|
||||
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")
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
if provider == "codex" {
|
||||
b.WriteString("## Codex-Specific Comment Formatting\n\n")
|
||||
b.WriteString("Codex often follows the per-turn reply command literally. For issue comments, always use `--content-stdin` with a HEREDOC, even for short single-line replies. ")
|
||||
@@ -158,7 +191,54 @@ 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")
|
||||
}
|
||||
|
||||
// Inject available repositories section.
|
||||
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")
|
||||
}
|
||||
|
||||
// Inject available repositories section. Per-workspace stable.
|
||||
if len(ctx.Repos) > 0 {
|
||||
b.WriteString("## Repositories\n\n")
|
||||
b.WriteString("The following code repositories are available in this workspace.\n")
|
||||
@@ -171,7 +251,8 @@ 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.
|
||||
// so skills can consume it programmatically. Per-issue: kept in the
|
||||
// dynamic suffix so it doesn't break prefix cache for project-less issues.
|
||||
if ctx.ProjectID != "" || len(ctx.ProjectResources) > 0 {
|
||||
b.WriteString("## Project Context\n\n")
|
||||
if ctx.ProjectTitle != "" {
|
||||
@@ -189,6 +270,9 @@ 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 != "" {
|
||||
@@ -267,74 +351,5 @@ 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)
|
||||
}
|
||||
|
||||
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("## 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