Skip to main content

locode_packs/
pack.rs

1//! The harness-pack abstraction (ADR-0012): a named toolset + a base prompt.
2
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use locode_host::Host;
7use locode_protocol::Message;
8use locode_tools::Registry;
9
10/// Dynamic, per-run context a pack's preamble is rendered against.
11///
12/// Deliberately small (like `ToolCtx`, ADR-0003 rejects god-object contexts): the fields
13/// a harness's real prompt needs (Task 13) — cwd/OS/shell/date + the headless identity
14/// branch. Grows only if a ported prompt needs more.
15#[derive(Debug, Clone)]
16pub struct PackContext {
17    /// Absolute working directory shown to the model.
18    pub cwd: PathBuf,
19    /// Target OS label (e.g. `macos`).
20    pub os: String,
21    /// Login shell (e.g. `/bin/zsh`).
22    pub shell: String,
23    /// Current date, preformatted (the preamble stays a pure function of context).
24    pub date: String,
25    /// Headless run → autonomous identity branch (vs interactive). See Task 13.
26    pub headless: bool,
27    /// Whether the working directory is inside a git repository. Rendered in the
28    /// Claude pack's `# Environment` block (`Is a git repository: <bool>`, D9).
29    /// Cheaply probed by the exec/tui layer (a `.git` walk) — no host handle in
30    /// `preamble()`.
31    pub is_git_repo: bool,
32    /// The model id/name for the Claude pack's env "You are powered by the model …"
33    /// line (D9). `None` skips the line (the pack does not guess).
34    pub model: Option<String>,
35    /// The OS version string for the Claude pack's env `OS Version:` line
36    /// (`uname -s -r`, computed by the exec/tui layer). `None` skips the line.
37    pub os_version: Option<String>,
38    /// The IANA timezone name for the codex pack's `<environment_context>`
39    /// `<timezone>` line (e.g. `America/Los_Angeles`), resolved best-effort by the
40    /// exec/tui layer. `None` omits the line (codex's field is optional).
41    pub timezone: Option<String>,
42    /// Strip identity-revealing sentences from the rendered prompt (e.g. grok's
43    /// "You are Grok released by xAI." and the `<user_guide>` block naming the
44    /// Grok Build TUI). **Default `false` = faithful reproduction** (user
45    /// decision, 2026-07-18); `true` is for A/B runs where the harness's
46    /// self-identity would contaminate the comparison. Post-processing on the
47    /// rendered output only — the verbatim template copies are never edited.
48    pub strip_identity: bool,
49}
50
51/// A faithful reproduction of one harness: its real toolset + its base prompt, selected
52/// whole via `--harness` (ADR-0012). One pack is active per run.
53///
54/// The pack — not a per-tool field — is the unit of harness identity (contrast Grok
55/// Build, which tags every tool with a namespace because it co-locates all harnesses'
56/// tools in one registry; we build a fresh registry per pack, so no tag is needed).
57pub trait Pack: Send + Sync {
58    /// The `--harness` selector and the report-envelope `harness` value.
59    fn name(&self) -> &'static str;
60
61    /// Register this pack's tools into `registry`, each under its harness's **real wire
62    /// name** (`Tool` has no name of its own — the name is assigned here). Tools are
63    /// constructed here holding an `Arc<Host>` (the only OS seam; `ToolCtx` is too small
64    /// to carry it). A duplicate name is a wiring bug and panics inside
65    /// `Registry::register`.
66    fn register(&self, host: &Arc<Host>, registry: &mut Registry);
67
68    /// The pack's **base preamble**: the ordered, role-tagged `System`/`Developer`
69    /// messages that seed the conversation (ADR-0013). Each pack maps its harness onto
70    /// our roles faithfully — a single `System` message, or `System` + `Developer`, etc.
71    /// The wire (Task 12) places each role in the right slot. Task 8 ships a scaffold;
72    /// the real content lands in Task 13.
73    fn preamble(&self, ctx: &PackContext) -> Vec<Message>;
74
75    /// The provider wire schemas this pack's tools require, or `None` when the pack
76    /// is wire-agnostic (grok, claude). The codex pack pins `["openai-responses"]`
77    /// because its `apply_patch` is a freeform (custom-grammar) tool that only
78    /// round-trips on the OpenAI Responses wire (ADR-0012, D5). Checked pre-run
79    /// against the resolved provider: a real schema not in the set fails before the
80    /// loop with an actionable message. The `mock` wire (keyless CI) is a universal
81    /// escape hatch and is always allowed, independent of this list.
82    fn required_api_schemas(&self) -> Option<&'static [&'static str]> {
83        None
84    }
85
86    /// Shape a raw user prompt the way this harness expects it. Some harnesses wrap
87    /// the task in a tag their system prompt refers to (grok's `<user_query>`);
88    /// Claude Code sends it verbatim. The default is verbatim; a pack overrides only
89    /// if its prompt is written against a wrapper. Keeps harness-specific shaping in
90    /// the pack rather than spread across the exec/tui layers.
91    fn shape_user_prompt(&self, text: &str) -> String {
92        text.to_owned()
93    }
94
95    /// Convenience: a fresh [`Registry`] holding exactly this pack's tools, over `host`.
96    ///
97    /// # Panics
98    /// If the pack's [`Pack::register`] assigns the same wire name twice.
99    fn build_registry(&self, host: &Arc<Host>) -> Registry {
100        let mut registry = Registry::new();
101        self.register(host, &mut registry);
102        registry
103    }
104}