Files
multica/server/internal/daemon/execenv/runtime_config_kind.go
Eve 38aa864e70 MUL-3560: refactor buildMetaSkillContent into kind-driven dispatcher
This is the structural prep PR for the runtime-brief diet roadmap on
MUL-3560 — split out from the per-section compression work so the
refactor risk and the content-gating risk land separately.

Output is byte-for-byte identical to the pre-refactor monolithic
builder for every existing test fixture. All existing tests pass
unchanged. The follow-up PR (0.6) will start gating sections per the
Section × Kind matrix from Eve's design comment on MUL-3560 (drop
Mentions / Comment Formatting / Issue Metadata / Sub-issue out of
quick-create, drop Comment Formatting / Mentions / Attachments out of
autopilot and chat, etc.), with negative assertions added alongside
each removal so each kind's brief contract becomes machine-checked.

What changed:

- runtime_config_kind.go (new): taskKind enum + classifyTask helper.
  Five kinds, mutually exclusive in practice; the documented
  precedence rule keeps the tiebreak deterministic even if a future
  caller breaks the mutex by accident. hasIssueContext is the
  predicate Issue Metadata and Sub-issue Creation gate on.
- runtime_config_sections.go (new): every section of the brief is
  extracted into its own writeXxx helper, preserving byte sequences
  exactly. Helpers with internal conditions keep those conditions;
  helpers the dispatcher always wants to call are unconditional.
- runtime_config.go: buildMetaSkillContent collapses from a
  ~450-line monolith to a ~70-line dispatcher — prelude, conditional
  context sections, a 5-way kind switch over Workflow bodies, and
  trailing sections. The matrix of "which kind gets which section"
  is now readable in one screen.
- runtime_config_kind_test.go (new): TestClassifyTask covers all 5
  kinds plus tiebreak cases; TestTaskKindHasIssueContext pins the
  predicate semantics; TestBuildMetaSkillContentKindMatrix is the
  structural canary that locks today's per-kind section set so any
  later PR that drops a section from a kind must update its
  expectations in lockstep.

Verification:

- go build ./...                                                      ok
- go vet ./internal/daemon/...                                        ok
- go test ./internal/daemon/... ./internal/handler/...                ok
- go test ./internal/daemon/execenv -run TestClassifyTask -v         pass
- go test ./internal/daemon/execenv -run TestTaskKindHasIssueContext pass
- go test ./internal/daemon/execenv -run TestBuildMetaSkillContent   pass

No production behaviour change in this PR. No new logging, no new
char-bucket fields — those are in PR #4439 and compose cleanly with
this refactor.

Co-authored-by: multica-agent <github@multica.ai>
2026-06-23 14:33:42 +08:00

98 lines
4.0 KiB
Go

package execenv
// taskKind labels the dispatch path that `buildMetaSkillContent` should follow
// for a given TaskContextForEnv. Today the brief contains several conditional
// sections gated by ad-hoc `ctx.ChatSessionID != ""` / `hasIssueContext` /
// `isAssignmentTriggered` checks scattered through `buildMetaSkillContent`.
// Centralising the classification into a single enum + helper gives every
// section a named axis to switch on (instead of re-deriving it from four
// pointers in each call site) and lets follow-up work apply strict per-kind
// gating without scattering more `if`s.
//
// This file is the structural prep for MUL-3560 PR 0.5 — see Eve's design
// reply on that issue:
//
// - kind classification: this file
// - per-section extraction + kind-driven dispatch in buildMetaSkillContent:
// runtime_config.go / runtime_config_sections.go
//
// The follow-up PR (0.6) will start removing sections that a given kind does
// not need (e.g. Mentions / Comment Formatting / Issue Metadata / Sub-issue
// out of quick-create); this PR keeps brief output byte-for-byte identical to
// the pre-refactor builder for every existing fixture so the refactor risk is
// isolated from the content-gating risk.
type taskKind int
const (
// kindCommentTriggered: a NEW comment on an issue triggered this run.
// `ctx.TriggerCommentID != ""` AND none of the chat / quick-create /
// autopilot fields are set. By far the most common kind.
kindCommentTriggered taskKind = iota
// kindAssignmentTriggered: an assignee was set / changed on an issue
// and the daemon fired a fresh run for the new assignee. No trigger
// comment, no chat / quick-create / autopilot context.
kindAssignmentTriggered
// kindAutopilotRunOnly: an autopilot fired in run-only mode (no issue
// is created or attached to this run; `ctx.AutopilotRunID != ""`).
kindAutopilotRunOnly
// kindQuickCreate: one-shot "create an issue from a natural-language
// prompt" task (`ctx.QuickCreatePrompt != ""`). There is no existing
// issue; the agent runs `multica issue create` exactly once and exits.
kindQuickCreate
// kindChat: interactive chat session (`ctx.ChatSessionID != ""`); no
// issue, no autopilot, no quick-create prompt.
kindChat
)
// classifyTask maps a TaskContextForEnv to the single taskKind that the brief
// should be assembled for. The ordering of the checks is the established
// precedence rule from the pre-refactor `buildMetaSkillContent`:
//
// 1. chat wins (ChatSessionID is the most-specific flag and runtime owners
// gate everything else off "not a chat" in the old code),
// 2. then quick-create,
// 3. then autopilot run-only,
// 4. then comment-triggered,
// 5. otherwise assignment-triggered.
//
// All five kinds are mutually exclusive at the call site that builds
// TaskContextForEnv — the daemon never sets two of ChatSessionID /
// QuickCreatePrompt / AutopilotRunID at once, and a comment trigger always
// implies an existing issue (TriggerCommentID is empty for on-assign). The
// precedence rule above is documented here only so a future caller that
// breaks the mutex by accident still falls into a deterministic kind instead
// of silently picking up the wrong workflow.
func classifyTask(ctx TaskContextForEnv) taskKind {
switch {
case ctx.ChatSessionID != "":
return kindChat
case ctx.QuickCreatePrompt != "":
return kindQuickCreate
case ctx.AutopilotRunID != "":
return kindAutopilotRunOnly
case ctx.TriggerCommentID != "":
return kindCommentTriggered
default:
return kindAssignmentTriggered
}
}
// hasIssueContext returns true for the kinds that operate on a real Multica
// issue and therefore can read / pin issue-scoped state (Issue Metadata,
// Sub-issue Creation, Project Context). Equivalent to the pre-refactor
// `ctx.ChatSessionID == "" && ctx.QuickCreatePrompt == "" && ctx.AutopilotRunID == ""`
// — extracted so the predicate has a name and every call site agrees on its
// meaning.
func (k taskKind) hasIssueContext() bool {
switch k {
case kindCommentTriggered, kindAssignmentTriggered:
return true
default:
return false
}
}