DEV Community

Hagicode
Hagicode

Posted on • Originally published at docs.hagicode.com

Teaching Every Agent to Speak `/goal`: A Compatible Extension Design for HagiCode's Continuous-Work Preset

Teaching Every Agent to Speak /goal: A Compatible Extension Design for HagiCode's Continuous-Work Preset

Introduction

When you hand an AI Agent a "continuous work" task, the ideal experience looks like this: you write down a goal, pick the repositories it's allowed to touch, click submit, and the Agent drives straight toward that goal—no mid-course questions, no going off-topic, no reaching outside the allowed repositories.

In HagiCode, this capability has a dedicated entry point—the goal preset task, whose command surface is /goal. The catch: not every Agent understands /goal. Claude and Codex have native continuous-work command semantics, so handing them /goal just works; but other CLIs (Gemini, Copilot, iFlow, OpenCode, etc.) have no such slash command at all. Stuffing /goal ... into them verbatim, at best, gets treated as an invalid command, and at worst gets executed literally and blows up.

The laziest option at first was: just let goal run on Claude / Codex only and reject every other Agent outright. But that shuts most of the Agent matrix out the door. This article is about how we made this actually work—using "Agent-aware" prompt variants, so natively supported Agents take the native command while unsupported Agents take a fallback prompt, keeping behavior aligned and the experience consistent.

The content is based on the goal preset package under repos/hagitask and the backend parsing implementation in repos/hagicode-core.

Background

What goal is

goal is a built-in task preset plugin package located at repos/hagitask/presets/goal/. Its manifest.json declares the plugin identity, icon, localization bundle, and frontend/backend assets:

{
  "taskPresetId": "goal",
  "version": "1.0.0",
  "icon": "flag",
  "kind": "custom-executor",
  "status": "experimental",
  "entrypoints": { "menuSurface": "session-create", "drawerId": "goal" },
  "ui": { "panel": "./frontend/panel.json", "commands": "./frontend/commands.json" },
  "backend": {
    "taskPreset": "./backend/task-preset.json",
    "prompts": "./backend/prompts.json"
  }
}
Enter fullscreen mode Exit fullscreen mode

Its purpose is "continuous work mode": the user fills in a goal description (goalDescription), optionally scopes the writable / read-only repositories, and then the backend assembles it into a single non-interactive auto-task run. The key fields in the frontend panel panel.json are:

  • goalCommandIds: the command selector, which by default contains only a single /goal;
  • goalDescription: the required goal description;
  • targetRepositories: the repository access selector, distinguishing read / write.

Where /goal comes from

The command surface is defined in frontend/commands.json, and the core is a single line of preludeTemplate:

{
  "commands": [
    {
      "id": "goal",
      "label": "goal",
      "preludeTemplate": "/goal {goalDescription}"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

In other words, the final prompt sent to the Agent will have a /goal <goal description> line prepended before the body. For Claude / Codex, this line is a command they recognize; for other Agents, this line is something that needs to be "translated." The adaptation challenge of this entire article is exactly this: how the meaning of that single /goal ... line diverges across different Agents.

Analysis

A single-locale template no longer holds up

HagiTask's early prompt packages were split by language only: in prompts.json, each locale declares one set of systemTemplate + userTemplate, and the backend PresetTaskCatalogProvider reads them into PromptPackage.Locales, then picks and renders one set at runtime based on the request language.

This model is enough for presets whose "execution instructions are Agent-agnostic." But goal isn't like that:

  • Claude / Codex: can rely directly on the native /goal command surface;
  • Other Agents: need a more verbose prompt that explicitly teaches them "how to enter continuous work mode," and must also tell them not to execute /goal ... as a command.

When two starkly different sets of instructions have to appear under the same locale, splitting by language alone is no longer enough. We needed to introduce another dimension beyond language: the Agent family.

Refusing to run isn't a good option

Initially, goal's requirements locked Agents down to ["claude", "codex"], and any other Agent was immediately judged incompatible. This has two product downsides:

  1. In the hero selection panel, the compatible heroes get filtered down to very few, easily leaving "no hero to pick";
  2. Plenty of Agents are perfectly capable of doing continuous work—they just don't have a native /goal—and a blanket rejection wastes their capability.

So the real problem to solve isn't "who can run," but "for the same goal, how do we give different but equivalent execution instructions based on Agent capability." The answer: push the Agent-aware differences down into the prompt layer, solving it with variants + fallbacks, rather than blocking people at the admission layer.

Solution

The variant selector: agent × language

The new prompt package contract introduces variants in prompts.json, using selectors to declare the matching dimensions and entries to declare each variant's matching rules and template files:

{
  "version": "1.1.0",
  "templateEngine": "handlebars",
  "variants": {
    "selectors": ["agent", "language"],
    "entries": [
      {
        "id": "native-zh-cn",
        "match": { "agent": ["claude", "codex"], "language": ["zh-CN"] },
        "systemTemplate": "./templates/zh-CN/native.system.md",
        "userTemplate": "./templates/zh-CN/native.user.hbs"
      },
      {
        "id": "fallback-zh-cn",
        "match": { "language": ["zh-CN"] },
        "systemTemplate": "./templates/zh-CN/fallback.system.md",
        "userTemplate": "./templates/zh-CN/fallback.user.hbs"
      }
      // ... native-en-us / fallback-en-us follow the same pattern
    ],
    "fallback": {
      "id": "fallback-default",
      "systemTemplate": "./templates/en-US/fallback.system.md",
      "userTemplate": "./templates/en-US/fallback.user.hbs"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Three levels of fallback, loosening progressively:

  1. native variant: match constrains both agent (claude/codex) and language at the same time, so only native Agents hit it;
  2. fallback variant: match constrains only language, not agent, so any Agent can hit it—this is the fallback for Agents that "have no native /goal";
  3. fallback-default: the final safety net for when even the language can't be matched.

Order matters: entries is traversed in declaration order, with native placed ahead of fallback. So claude/codex hit native first; other Agents fall through to the fallback language variant.

Two sets of templates: native vs. fallback

For the same goal, the difference between the two system prompts lies entirely in their attitude toward that /goal line.

The native template (templates/zh-CN/native.system.md):

You are the built-in execution prompt of the `goal` task preset plugin.

This run must use `/goal` as the command surface for continuous work mode. Treat the provided project paths and repository scope as authoritative work boundaries.

This run is in a non-interactive environment. Do not question the user; when reasonable assumptions allow you to keep making progress, proceed directly and state those assumptions explicitly in your response.
Enter fullscreen mode Exit fullscreen mode

The fallback template (templates/zh-CN/fallback.system.md):

You are the fallback execution prompt of the `goal` task preset plugin.

The current agent may not support the native `/goal` command. If a `/goal ...` line appears at the front of the prompt, treat it as goal metadata attached by the preset—do not treat it as a slash command that must be executed verbatim.

Please complete the task in continuous work mode, and treat the provided project paths and repository scope as authoritative work boundaries.

This run is in a non-interactive environment. Do not question the user; when reasonable assumptions allow you to keep making progress, proceed directly and state those assumptions explicitly in your response.
Enter fullscreen mode Exit fullscreen mode

That core sentence is the soul of the fallback strategy: "If a /goal ... line appears at the front of the prompt, treat it as goal metadata attached by the preset—do not treat it as a slash command that must be executed verbatim." The /goal <goal> line that preludeTemplate splices in is a command in the eyes of a native Agent, but for a fallback Agent it is explicitly downgraded to "metadata / a goal marker," so that ordinary prompt following reproduces the same continuous-work behavior.

The user templates (.hbs) also come in two sets, and the fallback set has an extra "routing note" section that explicitly tells the Agent it was routed to a branch that doesn't rely on native /goal, and requires it to "not rely on native /goal support; instead use ordinary prompt-following to accomplish the same continuous-work behavior."

Backend: one resolution decides it all

Variant matching happens in the backend at ResolvePromptSelection in PCode.Application/Services/PresetTaskCatalogProvider.cs. It first resolves the locale, then packs language and agent into a selector dictionary, and finds the first matching variant in order:

public PresetTaskResolvedPromptSelection ResolvePromptSelection(string? locale, string? agentFamily)
{
    var resolvedLocale = ResolvePromptLocale(locale);
    if (!PromptPackage.UsesVariants)
    {
        // Legacy presets split by locale only take the legacy path, preserving backward compatibility
        var legacyTemplate = PromptPackage.Locales[resolvedLocale];
        return new PresetTaskResolvedPromptSelection(
            resolvedLocale, NormalizeAgentFamily(agentFamily), null, false,
            "legacy-locales", legacyTemplate);
    }

    var normalizedAgentFamily = NormalizeAgentFamily(agentFamily);
    var selectorInputs = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        [PresetTaskPromptSelectorNames.Language] = resolvedLocale,
    };
    if (!string.IsNullOrWhiteSpace(normalizedAgentFamily))
    {
        selectorInputs[PresetTaskPromptSelectorNames.Agent] = normalizedAgentFamily!;
    }

    foreach (var variant in PromptPackage.Variants)
    {
        if (!VariantMatches(variant, selectorInputs)) continue;
        return new PresetTaskResolvedPromptSelection(
            resolvedLocale, normalizedAgentFamily, variant.Id, false, "variants", variant.Template);
    }

    // No variant hit: fall to the explicitly declared fallback variant
    return new PresetTaskResolvedPromptSelection(
        resolvedLocale, normalizedAgentFamily, PromptPackage.FallbackVariant?.Id, true,
        "variants", PromptPackage.FallbackVariant?.Template ?? PromptPackage.Locales[resolvedLocale]);
}
Enter fullscreen mode Exit fullscreen mode

The matching rule VariantMatches is dead simple—whatever selectors a variant declares, they all have to match at runtime; undeclared dimensions impose no constraint:

private static bool VariantMatches(
    PresetTaskPromptVariantDefinition variant,
    IReadOnlyDictionary<string, string> selectorInputs)
{
    foreach (var selector in variant.Match)
    {
        if (!selectorInputs.TryGetValue(selector.Key, out var runtimeValue))
            return false;
        if (!selector.Value.Contains(runtimeValue, StringComparer.OrdinalIgnoreCase))
            return false;
    }
    return true;
}
Enter fullscreen mode Exit fullscreen mode

This explains why a fallback variant that only writes language can cover all Agents: it doesn't constrain the agent dimension at all, so any Agent whose language matches will hit it. The native variant, by contrast, adds one more constraint on agent, so only claude/codex can satisfy it.

Where does the Agent family come from? At runtime it's mapped from the selected hero's executor type—see PresetTaskRequirementModels.cs:

public static string? Resolve(AIProviderType? executorType) => executorType switch
{
    AIProviderType.ClaudeCodeCli => Claude,
    AIProviderType.CodexCli      => Codex,
    AIProviderType.GitHubCopilot => Copilot,
    AIProviderType.IFlowCli      => Iflow,
    AIProviderType.OpenCodeCli   => Opencode,
    // ... gemini / kimi / qoder / kiro / hermes / codebuddy / pi ...
    _ => null,
};
Enter fullscreen mode Exit fullscreen mode

When creating a session, SessionsController.PresetTasks.cs first gets the hero, then calls PresetTaskAgentFamilies.Resolve(hero.ExecutorType) to get the family name and feeds it to ResolvePromptSelection. After a variant is selected, RenderPrompt renders the template with Handlebars and splices the /goal <goal> generated by preludeTemplate right at the front:

var renderedPrompt = prompt.FillTemplate(normalizedContext);
var commandPrelude = BuildCommandPrelude(normalizedContext); // -> "/goal <goalDescription>"
return string.IsNullOrWhiteSpace(commandPrelude)
    ? renderedPrompt
    : $"{commandPrelude}\n\n{renderedPrompt}";
Enter fullscreen mode Exit fullscreen mode

Loosening admission: from an allowlist to any

With a fallback prompt in place, goal no longer needs to shut non-native Agents out. The requirements in backend/task-preset.json loosens from ["claude","codex"] to any:

{
  "taskKey": "goal",
  "requirements": [ { "type": "agent", "name": "any" } ],
  "inputBindings": [
    { "input": "goalCommandIds",  "promptParameter": "goalCommandIds",  "required": true },
    { "input": "goalDescription", "promptParameter": "goalDescription", "required": true },
    { "input": "targetScopeMarkdown", "promptParameter": "targetScopeMarkdown", "producer": "frontend-computed" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Open up the admission layer, push the differences down to the prompt layer—this is the key trade-off of the whole design: compatibility is no longer a switch of "can it run," but a routing of "which prompt set does it run with."

Practice

What an end-to-end run looks like

  1. In the goal drawer, the user fills in the goal description, selects the repository scope, picks a hero, and submits;
  2. The backend resolves the agent family (e.g. claude / gemini) from the selected hero's executor type;
  3. ResolvePromptSelection(locale, agentFamily) finds a variant by agent × language:
    • claude/codex + zh-CN → native-zh-cn;
    • gemini + zh-CN → native doesn't hit, falls to fallback-zh-cn;
  4. RenderPrompt renders the selected template and splices /goal <goal description> at the front;
  5. A native Agent executes the first line as a command; a fallback Agent, following the system prompt, treats the first line as metadata and reproduces continuous work via ordinary prompt following.

Adding Agent awareness to your own preset

If you want to do the same adaptation for a new preset, the routine is fixed:

  1. In prompts.json, replace locales with variants, and add agent to selectors;
  2. The native variant's match writes both agent and language; the fallback variant writes only language;
  3. Then declare a top-level variants.fallback as the final safety net (the schema requires that the variants mode must have a fallback);
  4. The backend needs no changes—ResolvePromptSelection / VariantMatches are generic;
  5. If admission shouldn't be restricted, set the requirements agent to any.

A few easy pitfalls

  • Variant order is priority. native must come before fallback, otherwise the fallback that only constrains language would grab all Agents first, and claude/codex would never reach the native branch.
  • The fallback prompt must explicitly "downgrade" that /goal line. The /goal ... prefix is spliced on unconditionally (decided by preludeTemplate), so the sentence in the fallback system prompt—"treat it as metadata, don't execute it verbatim"—cannot be omitted, or a non-native Agent might treat it as an invalid command.
  • The non-interactive constraint must go into every template. goal is a queued auto-task, and the templates repeatedly emphasize "don't use AskUserQuestion, don't question, proceed with reasonable assumptions and state them," precisely to keep the Agent from getting stuck waiting for input while unattended.
  • Repository boundaries are enforced by the prompt, not by a sandbox. The templates explicitly require "only modify within repositories explicitly selected as writable, and treat read-only repositories as supplementary context." This is a soft constraint at the prompt layer, so fill in the repository scope carefully.
  • Backward compatibility is a free lunch. When an old preset writes no variants and only writes locales, UsesVariants is false, and it goes straight down the legacy-locales path—completely unaffected—so you don't have to touch existing presets just for the new model.

Conclusion

"If an Agent doesn't natively support goal, how do we extend it to make it work"—the answer isn't to hard-code a pile of branches for each Agent, but to model the difference as agent × language prompt variants:

  • Natively supported Agents (Claude / Codex) take the native variant and use the /goal command surface directly;
  • Unsupported Agents take the fallback variant, explicitly downgrade /goal ... to goal metadata, and reproduce the same continuous-work behavior via ordinary prompt following;
  • Matching relies on the generic rule that "whatever selectors a variant declares must all match" + progressive fallbacks, resolved once and for all on the backend;
  • Admission loosens from an allowlist to any, so compatibility shifts from "can it run" to "which prompt set does it run with."

The value of this design isn't limited to the single goal preset: the agent × language selector + fallback model is generic, reusable by any preset that needs to "give different instructions based on Agent capability," and with zero disruption to old presets.

References

  • Preset package: repos/hagitask/presets/goal/ (manifest.json, backend/task-preset.json, backend/prompts.json, backend/templates/{en-US,zh-CN}/{native,fallback}.*, frontend/commands.json, frontend/panel.json)
  • Prompt package schema: repos/hagitask/schemas/task-preset-plugin/prompt-package.schema.json
  • Backend variant resolution: repos/hagicode-core/src/PCode.Application/Services/PresetTaskCatalogProvider.cs (ResolvePromptSelection / VariantMatches / RenderPrompt / BuildCommandPrelude)
  • Agent family mapping: repos/hagicode-core/src/PCode.Application/Services/PresetTaskRequirementModels.cs (PresetTaskAgentFamilies.Resolve)
  • Session creation: repos/hagicode-core/src/PCode.HttpApi/Controllers/SessionsController.PresetTasks.cs
  • OpenSpec capability and proposals: openspec/specs/goal-preset-task/spec.md, openspec/changes/archive/2026-06-23-preset-task-agent-requirement-support/, openspec/changes/archive/2026-06-26-support-agent-language-prompt-selection-in-hagitask/

Original Article & License

Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.
This article was created with AI assistance and reviewed by the author before publication.

Top comments (0)