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}