// bundle.go implements promptkit.PromptBundle for the English Claude Code // preset. Three layers per ADR-0005 § 2.3: // // Layer 1 - BaseSections: 6 always-on sections (cache-stable prefix). // Layer 2 - ConditionalSections: coding tools + git protocol + FLYTO.md // + env_info, gated by BuildContext predicates. // Layer 3 - ReminderTriggers: empty for now (C7 wires reminder system). // // bundle.go 实现 promptkit.PromptBundle 给英文 Claude Code preset. 三层 // 见 ADR-0005 § 2.3: // Layer 1 - BaseSections: 6 段 always-on (cache 稳定前缀). // Layer 2 - ConditionalSections: coding tools + git protocol + FLYTO.md // + env_info, 由 BuildContext 谓词控制. // Layer 3 - ReminderTriggers: 当前空, C7 接通 reminder 系统. package presetcoding import ( "fmt" "strings" agentctx "git.flytoex.net/yuanwei/flyto-agent/pkg/context" "git.flytoex.net/yuanwei/flyto-agent/pkg/promptkit" ) // EnglishBundle is the Claude Code-style English coding preset implementation // of promptkit.PromptBundle. Use NewEnglishBundle to construct. // // EnglishBundle 是英文 Claude Code 编程预设的 promptkit.PromptBundle 实现. // 用 NewEnglishBundle 构造. type EnglishBundle struct{} // NewEnglishBundle returns a new English Claude Code preset bundle. // // NewEnglishBundle 返回一个英文 Claude Code preset bundle 实例. func NewEnglishBundle() *EnglishBundle { return &EnglishBundle{} } // BaseSections returns the 6 always-on sections forming the cache-stable // prefix. Mirror cc Layer 1: intro / system / doing_tasks / actions / // tone_and_style / output_efficiency. // // BaseSections 返回 6 段 always-on 形成 cache 稳定前缀. mirror cc Layer 1. // // CLEVER: order matters for prompt cache prefix matching. Anthropic API's // ephemeral cache hashes the prefix bytes, so any reordering invalidates // existing cache slots. Order is locked to mirror cc verbatim. // // CLEVER: 顺序对 prompt cache 前缀匹配很关键. Anthropic API ephemeral // cache 哈希前缀字节, 任何重排都让现有 cache slot 失效. 顺序锁定 mirror // cc 一字不差. func (b *EnglishBundle) BaseSections() []promptkit.Section { return []promptkit.Section{ &staticSection{name: "intro", body: sectionIntro}, &staticSection{name: "system", body: sectionSystem}, &staticSection{name: "doing_tasks", body: sectionDoingTasks}, &staticSection{name: "actions", body: sectionActions}, &staticSection{name: "tone_and_style", body: sectionToneAndStyle}, &staticSection{name: "output_efficiency", body: sectionOutputEfficiency}, } } // ConditionalSections returns sections gated by BuildContext predicates. // Order: coding_tools (always for coding preset) → search_code (always) → // git_protocol (when HasGitRepo) → flyto_md (when HasFLYTOMD) → // env_info (always, computed from Cwd) → summarize_tool_results (always). // // ConditionalSections 返回由 BuildContext 谓词控制的段. 顺序: coding_tools // (编程预设始终) → search_code (始终) → git_protocol (HasGitRepo) → // flyto_md (HasFLYTOMD) → env_info (始终, 从 Cwd 计算) → summarize_tool_results. // // CLEVER: coding_tools and search_code are technically always-on for the // coding preset (they describe Read/Edit/Bash/Glob/Grep semantics). They // live in Layer 2 not Layer 1 because non-coding presets won't include // them — the conditional placement is "non-base" in the abstract sense // even when this specific preset always emits them. // // CLEVER: coding_tools 和 search_code 对编程预设事实上是 always-on (描述 // Read/Edit/Bash/Glob/Grep 语义). 放 Layer 2 不放 Layer 1 是因为非编程 // preset 不会包含它们 — conditional 放置是抽象上的"非 base", 即使这个具体 // preset 始终发出它们. func (b *EnglishBundle) ConditionalSections(ctx promptkit.BuildContext) []promptkit.Section { return []promptkit.Section{ &staticSection{name: "using_tools", body: sectionUsingTools}, &staticSection{name: "search_code", body: sectionSearchCode}, &gitProtocolSection{}, &flytoMdSection{}, &envInfoSection{}, &staticSection{name: "summarize_tool_results", body: sectionSummarizeToolResults}, } } // ReminderTriggers returns the runtime reminder hooks. Empty for C4 — the // reminder system rewire (Bug H fix) lands in C7. After C7 this returns // the trigger list for file-read-malware, mtime-drift, etc. // // ReminderTriggers 返回运行时 reminder hook. C4 阶段为空 — reminder 系统 // 重接 (修 Bug H) 在 C7 落. C7 后返回 file-read-malware / mtime-drift 等 // trigger 列表. func (b *EnglishBundle) ReminderTriggers() []promptkit.Trigger { return nil } // Verify EnglishBundle implements promptkit.PromptBundle at compile time. // 编译期验证 EnglishBundle 实现 promptkit.PromptBundle. var _ promptkit.PromptBundle = (*EnglishBundle)(nil) // --------------------------------------------------------------------------- // Section implementations // 段实现 // --------------------------------------------------------------------------- // staticSection renders a fixed string regardless of Context. Used for // content that doesn't depend on session state. // // staticSection 不管 Context 都返回固定字符串. 给不依赖会话状态的内容用. type staticSection struct { name string body string } func (s *staticSection) Name() string { return s.name } func (s *staticSection) Render(_ promptkit.Context) (string, error) { return s.body, nil } // gitProtocolSection emits the git safety protocol only when a git repo // is detected at or above Cwd. Implements Bug F + I fix from ADR-0005: // non-git callers (e.g. quote-engine-probe extracting from CSV in /tmp) // no longer get the git protocol pollution. // // gitProtocolSection 仅在检测到 Cwd 或祖先目录有 git 仓库时发 git 协议. // 实现 ADR-0005 Bug F + I 修复: 非 git 调用方 (例如 quote-engine-probe 在 // /tmp 抽 CSV) 不再被 git 协议污染. type gitProtocolSection struct{} func (s *gitProtocolSection) Name() string { return "git_protocol" } func (s *gitProtocolSection) Render(ctx promptkit.Context) (string, error) { if !ctx.HasGitRepo { return "", nil } return sectionGitProtocol, nil } // flytoMdSection loads FLYTO.md from the standard search paths and emits // it as a "Custom Instructions" section. Returns empty when no FLYTO.md // found anywhere — non-Flyto-aware callers don't see this pollution. // // flytoMdSection 从标准搜索路径加载 FLYTO.md 当作 "Custom Instructions" // 段发出. 任何位置都没找到则返回空 — 不感知 Flyto 的调用方看不到此污染. // // LEGACY: agentctx.LoadInstructions remains the source of truth during // the migration window (C2-C11). C12 may relocate the loader to // preset-coding/conditional/ as part of the prompts.go deletion sweep. // // LEGACY: agentctx.LoadInstructions 在迁移窗口 (C2-C11) 仍是真相源. C12 // prompts.go 删除时可能把 loader 搬到 preset-coding/conditional/. type flytoMdSection struct{} func (s *flytoMdSection) Name() string { return "flyto_md" } func (s *flytoMdSection) Render(ctx promptkit.Context) (string, error) { if ctx.Cwd == "" { return "", nil } instructions := agentctx.LoadInstructions(ctx.Cwd) if instructions == "" { return "", nil } return "# Custom Instructions (from FLYTO.md)\n\n" + instructions, nil } // envInfoSection emits the runtime environment info (cwd / platform / // shell / OS version / git branch+status). Always-on because every // session has a Cwd; gracefully empty if Cwd is "". // // envInfoSection 发出运行时环境信息 (cwd / platform / shell / OS 版本 / // git 分支+状态). always-on 因为每会话都有 Cwd; Cwd 为 "" 时优雅返回空. // // LEGACY: agentctx.CollectEnvInfo remains the source of truth during the // migration window. Same C12 relocation note as flytoMdSection. // // LEGACY: agentctx.CollectEnvInfo 在迁移窗口仍是真相源. C12 搬迁同 // flytoMdSection. type envInfoSection struct{} func (s *envInfoSection) Name() string { return "env_info" } func (s *envInfoSection) Render(ctx promptkit.Context) (string, error) { if ctx.Cwd == "" { return "", nil } info := agentctx.CollectEnvInfo(ctx.Cwd) var sb strings.Builder sb.WriteString("# Environment\n\n") sb.WriteString(fmt.Sprintf(" - Primary working directory: %s\n", info.Cwd)) isGitStr := "false" if info.IsGitRepo { isGitStr = "true" } sb.WriteString(fmt.Sprintf(" - Is a git repository: %s\n", isGitStr)) sb.WriteString(fmt.Sprintf(" - Platform: %s\n", info.Platform)) if info.Shell != "" { sb.WriteString(fmt.Sprintf(" - Shell: %s\n", info.Shell)) } if info.OSVersion != "" { sb.WriteString(fmt.Sprintf(" - OS Version: %s\n", info.OSVersion)) } if info.IsGitRepo && info.GitBranch != "" { sb.WriteString(fmt.Sprintf(" - Git branch: %s\n", info.GitBranch)) } if info.IsGitRepo && info.GitStatus != "" { sb.WriteString(fmt.Sprintf(" - Git status: %s\n", info.GitStatus)) } return strings.TrimRight(sb.String(), "\n"), nil }