Skip to main content

eval_magic/adapters/
harness.rs

1//! The harness adapter API — the single seam between generic dispatch code and
2//! harness-specific behavior.
3//!
4//! The trait is tiered into a **baseline** every harness must implement and
5//! **enhancements** that raise fidelity when a harness has the native support:
6//!
7//! - **Baseline (required):** [`label`](HarnessAdapter::label) and
8//!   [`skills_dir`](HarnessAdapter::skills_dir). A new harness compiles with
9//!   just these two methods; dispatched through its one-shot CLI (with
10//!   `--no-stage` inlining the skill when native staging isn't wired), it
11//!   already supports `llm_judge` grading and the `detect-stray-writes`
12//!   post-pass.
13//! - **Enhancements (defaulted):** every other method has a default — either a
14//!   working generic fallback (e.g. the plain available-skills block) or an
15//!   `Unsupported` error naming the enhancement it belongs to (e.g. transcript
16//!   ingest, the write guard). Override the methods of an enhancement to wire
17//!   it for a harness.
18//!
19//! Generic code resolves an adapter with [`adapter_for`](super::registry::adapter_for)
20//! and then calls the trait — so the [`registry`](super::registry) is the one
21//! place that names a concrete harness for this surface. The impls live in the
22//! per-harness modules ([`claude_code`](super::claude_code),
23//! [`codex`](super::codex), [`opencode`](super::opencode)).
24
25use std::io;
26use std::path::{Path, PathBuf};
27use std::time::Duration;
28
29use crate::core::{AvailableSkill, HarnessRunCapabilities, ToolInvocation};
30use crate::sandbox::GuardMarker;
31
32use super::TranscriptSummary;
33use super::skill_shadow::PluginShadowReport;
34
35/// One harness's tool-name vocabulary: every name its guard hook payloads or
36/// transcript parser can produce, grouped by role. Consumers match against the
37/// union across all harnesses ([`all_tool_vocabulary`](super::registry::all_tool_vocabulary)).
38#[derive(Debug, Clone, Default, PartialEq, Eq)]
39pub struct ToolVocabulary {
40    /// Tools that write the filesystem with a single target path argument.
41    pub write_tools: Vec<String>,
42    /// apply_patch-style tools whose payload carries multiple patch targets.
43    pub patch_tools: Vec<String>,
44    /// Shell-execution tools carrying a `command` argument.
45    pub shell_tools: Vec<String>,
46    /// Read-only tools carrying a target path argument.
47    pub read_tools: Vec<String>,
48}
49
50/// The behavior that varies by harness. Generic dispatch code depends on this
51/// trait, never on a concrete harness variant. See the module docs for the
52/// baseline-vs-enhancement contract.
53pub trait HarnessAdapter {
54    // ── Baseline (required) — every harness implements these ────────────────
55
56    /// **Baseline.** The kebab-case identifier used in CLI flags,
57    /// `dispatch.json`, and the staged `conditions.json`.
58    fn label(&self) -> String;
59
60    /// **Baseline.** The project-local directory staged skills live under for
61    /// this harness. Under `--no-stage` nothing is staged into it, so a
62    /// baseline harness may point this at any repo-local path its discovery
63    /// would read. `None` when the harness declares no skills directory —
64    /// native staging is then unavailable and the run preflight forces
65    /// `--no-stage` (each SKILL.md is inlined into its dispatch prompt).
66    fn skills_dir(&self, repo_root: &Path) -> Option<PathBuf>;
67
68    // ── Run-option capabilities (defaulted) ──────────────────────────────────
69
70    /// The run options the generic `run` preflight may accept for this
71    /// harness. The default is the baseline: no write guard, `--bootstrap` and
72    /// `--stage-name` allowed alongside `--no-stage`. Override alongside the
73    /// enhancement that changes support (e.g. wiring the write guard flips
74    /// `supports_guard`).
75    fn run_capabilities(&self) -> HarnessRunCapabilities {
76        HarnessRunCapabilities {
77            supports_guard: false,
78            supports_bootstrap_with_no_stage: true,
79            supports_stage_name_with_no_stage: true,
80        }
81    }
82
83    /// The project-local config dir names this harness reads or the adapter
84    /// writes (e.g. `.claude`). Staging excludes every harness's config dirs
85    /// when copying a skill's sibling assets, so a stray checked-in config dir
86    /// never rides into a staged env. Via
87    /// [`all_config_dir_names`](super::registry::all_config_dir_names) this list
88    /// also feeds the guard's Bash tamper rule and detect-stray-writes'
89    /// staging-dir lookbehind, so adding a dir here automatically grows the
90    /// write-guard's deny surface. List the parent of
91    /// [`skills_dir`](Self::skills_dir) plus any hook/config dirs the adapter
92    /// writes.
93    fn config_dir_names(&self) -> Vec<String> {
94        Vec::new()
95    }
96
97    /// The tool names this harness's guard hook payloads and parsed transcripts
98    /// use, grouped by role. Via
99    /// [`all_tool_vocabulary`](super::registry::all_tool_vocabulary) this feeds the guard
100    /// arbiter's tool classification and detect-stray-writes' invocation audit,
101    /// so list every name this harness's surfaces produce — even names another
102    /// harness also uses; the union dedups. Default empty: a harness with no
103    /// guard and no transcript parser contributes nothing.
104    fn tool_vocabulary(&self) -> ToolVocabulary {
105        ToolVocabulary::default()
106    }
107
108    // ── Enhancement: native skill staging (defaulted) ────────────────────────
109    // Fallback without it: `--no-stage` inlines each SKILL.md into its
110    // dispatch prompt instead of staging files for native discovery.
111
112    /// **Enhancement: native staging.** Build the conspicuous staged-skill
113    /// slug. The default underscore form is fine for any harness without
114    /// naming rules; a harness with constrained skill names (e.g. OpenCode)
115    /// overrides it. `prefix` must be preserved so cleanup prefix-scans still
116    /// find the staged dir.
117    fn staged_slug(
118        &self,
119        prefix: &str,
120        iteration: u32,
121        condition: &str,
122        skill_name: &str,
123    ) -> String {
124        format!("{prefix}{iteration}-{condition}__{skill_name}")
125    }
126
127    /// **Enhancement: native staging.** Validate a staged-skill identifier
128    /// (generated slug or `--stage-name` override) against this harness's
129    /// naming rules. The default accepts anything.
130    fn validate_stage_name(&self, _name: &str) -> Result<(), String> {
131        Ok(())
132    }
133
134    /// **Enhancement: native staging.** Whether a staged skill's frontmatter
135    /// `name:` is rewritten to its slug so the harness's repo-local discovery
136    /// resolves the staged copy.
137    fn rewrites_frontmatter_name(&self) -> bool {
138        false
139    }
140
141    /// **Enhancement: native staging.** Whether the skill-under-test is
142    /// advertised in the available-skills block under its staged slug (vs. its
143    /// natural name). True for Codex and OpenCode, whose repo-local discovery
144    /// keys on the rewritten frontmatter name.
145    fn advertises_staged_slug_name(&self) -> bool {
146        false
147    }
148
149    /// **Enhancement: native staging.** Render the discoverable skills the way
150    /// this harness natively surfaces them (e.g. Claude Code's Skill-tool
151    /// list, Codex's `## Skills`, OpenCode's `<available_skills>` XML). The
152    /// default is a neutral bulleted list.
153    fn render_available_skills_block(&self, skills: &[AvailableSkill]) -> String {
154        super::skills_block::render_skills_block(
155            super::skills_block::DEFAULT_HEADER,
156            super::skills_block::DEFAULT_ITEM,
157            "",
158            skills,
159        )
160    }
161
162    /// **Enhancement: native staging.** How a staged skill is described as
163    /// discoverable in the neutral slug-disambiguation line (e.g. "via the
164    /// Skill tool").
165    fn skill_surface_phrase(&self) -> String {
166        "as a discoverable skill".to_string()
167    }
168
169    /// **Enhancement: native staging.** The lead-in for the fallback "read the
170    /// skill from `<path>`" instruction when the staged identifier can't be
171    /// resolved.
172    fn skill_unresolved_phrase(&self) -> String {
173        "If the staged skill cannot be resolved".to_string()
174    }
175
176    // ── Enhancement: transcript parser (defaulted) ───────────────────────────
177    // Fallback without it: `transcript_check` assertions grade as
178    // unverifiable, `llm_judge` carries the grading, token/cost/duration go
179    // unrecorded, and run records are assembled by hand (or from
180    // `outputs/final-message.md`) instead of auto-ingested.
181
182    /// **Enhancement: transcript parser.** The filename (under a task's
183    /// `outputs/` dir) this harness's one-shot CLI writes the captured
184    /// transcript to. `None` when no transcript ingest is wired — the ingest
185    /// pipeline then never calls the parsers below.
186    fn cli_events_filename(&self) -> Option<String> {
187        None
188    }
189
190    /// **Enhancement: transcript parser.** Parse the events file this
191    /// harness's one-shot CLI wrote (the captured transcript) into ordered
192    /// tool invocations.
193    fn parse_cli_events(&self, _path: &Path) -> io::Result<Vec<ToolInvocation>> {
194        Err(io::Error::new(
195            io::ErrorKind::Unsupported,
196            format!(
197                "transcript ingest is not wired for the {} harness",
198                self.label()
199            ),
200        ))
201    }
202
203    /// **Enhancement: transcript parser.** The full-summary counterpart of
204    /// [`parse_cli_events`](Self::parse_cli_events): tool invocations, deduped
205    /// token usage, duration, and final message text.
206    fn parse_cli_events_full(&self, _path: &Path) -> io::Result<TranscriptSummary> {
207        Err(io::Error::new(
208            io::ErrorKind::Unsupported,
209            format!(
210                "transcript ingest is not wired for the {} harness",
211                self.label()
212            ),
213        ))
214    }
215
216    /// **Enhancement: transcript parser.** The deterministic skill-invocation
217    /// signature the `__skill_invoked` meta-check matches: `(tool name, arg
218    /// carrying the staged slug)` — Claude Code's `Skill`/`skill`, OpenCode's
219    /// `skill`/`name`. `None` for Codex (its JSONL has no skill-tool event),
220    /// which routes the meta-check to the LLM-judge fallback.
221    fn transcript_skill_invocation(&self) -> Option<(String, String)> {
222        Some(("Skill".to_string(), "skill".to_string()))
223    }
224
225    // ── Enhancement: model flag (defaulted) ──────────────────────────────────
226    // Fallback without it: `--agent-model` / `--judge-model` are recorded as
227    // provenance only; dispatches run on the harness's default model.
228
229    /// **Enhancement: model flag.** The native model-selection flag accepted
230    /// by this harness's CLI. `None` means no model-selection support is
231    /// wired.
232    fn cli_model_flag(&self) -> Option<String> {
233        None
234    }
235
236    // ── Enhancement: write guard (defaulted) ─────────────────────────────────
237    // Fallback without it: the `detect-stray-writes` post-pass (folded into
238    // `ingest`) audits out-of-bounds writes after the fact.
239
240    /// **Enhancement: write guard.** Arm the write guard using this harness's
241    /// native pre-tool hook surface, returning the staged marker path. The
242    /// guard's allowed roots are derived from `stage_root` (the isolated env /
243    /// agent cwd), so it bounds the agent to the same env boundary that
244    /// isolates its reads.
245    fn install_guard(
246        &self,
247        _stage_root: &Path,
248        _guard_exe: &Path,
249        _ttl: Option<Duration>,
250    ) -> io::Result<PathBuf> {
251        Err(io::Error::new(
252            io::ErrorKind::Unsupported,
253            format!("--guard is not supported for the {} harness", self.label()),
254        ))
255    }
256
257    /// **Enhancement: write guard.** The banner printed after `--guard`
258    /// successfully arms, describing the harness's native hook surface and how
259    /// to remove it. `None` for a harness with no write guard (its
260    /// [`install_guard`](Self::install_guard) errors), in which case no banner
261    /// is printed.
262    fn guard_armed_message(&self) -> Option<String> {
263        None
264    }
265
266    /// **Enhancement: write guard.** Evaluate a PreToolUse hook `payload`
267    /// against `marker`, returning the serialized deny verdict to print on
268    /// stdout, or `None` to allow. The default fails open — a harness with no
269    /// guard never denies — matching the hook entry points' contract that a
270    /// guard invocation can never brick a session.
271    fn guard_verdict(&self, _payload: &str, _marker: Option<GuardMarker>) -> Option<String> {
272        None
273    }
274
275    /// **Enhancement: write guard.** A hook-config dir the guard install
276    /// created outside [`skills_dir`](Self::skills_dir) (e.g. Codex's
277    /// `.codex/`), which teardown prunes when restoring the original config
278    /// leaves it empty. `None` when the guard writes only under existing dirs.
279    fn guard_hook_cleanup_dir(&self, _stage_root: &Path) -> Option<PathBuf> {
280        None
281    }
282
283    // ── Enhancement: shadow preflight (defaulted) ────────────────────────────
284    // Fallback without it: no preflight — the run proceeds with no shadow
285    // report, exactly as for a harness whose dispatches load nothing global.
286
287    /// **Enhancement: shadow preflight.** Detect staged skill names that are
288    /// also discoverable from the operator's live environment (e.g. Claude
289    /// Code's enabled plugins or global skills dir), which contaminates the
290    /// with/without comparison. `scan_root` is a real staged env root — its
291    /// project-local settings participate in detection. `None` when the
292    /// harness has no shadow preflight (the default) or nothing is shadowed.
293    fn detect_shadowed_skills(
294        &self,
295        _scan_root: &Path,
296        _staged_skill_names: &[&str],
297    ) -> Option<PluginShadowReport> {
298        None
299    }
300
301    /// **Enhancement: shadow preflight.** Format the runner banner for a
302    /// report. The default preserves the original Claude-oriented rendering
303    /// for third-party adapters and older artifacts.
304    fn format_shadow_banner(&self, report: &PluginShadowReport) -> String {
305        super::skill_shadow::format_shadow_banner(report)
306    }
307
308    /// **Enhancement: shadow preflight.** Format aggregate validity warnings
309    /// for a report. The default preserves the original Claude-oriented
310    /// rendering for third-party adapters and older artifacts.
311    fn shadow_validity_warnings(&self, report: &PluginShadowReport) -> Vec<String> {
312        super::skill_shadow::shadow_validity_warnings(report)
313    }
314
315    // ── Enhancement: plan-mode context (defaulted) ───────────────────────────
316
317    /// **Enhancement: plan-mode context.** Wrap a plan-mode profile as an
318    /// operating-context layer. The shared `<system-reminder>` default
319    /// usually suffices; a harness with a real native plan mode could inject
320    /// it differently.
321    fn render_plan_mode_context(&self, profile_text: &str) -> String {
322        let trimmed = profile_text.trim();
323        if trimmed.is_empty() {
324            return String::new();
325        }
326        format!("<system-reminder>\n{trimmed}\n</system-reminder>")
327    }
328
329    // ── Enhancement: dispatch recipes (defaulted) ────────────────────────────
330    // Fallback without them: `run` prints the generic handoff and the runbook
331    // carries no copy-pasteable per-task command.
332
333    /// **Enhancement: dispatch recipes.** Whether a copy-pasteable per-task
334    /// exec command is wired (the descriptor's `[dispatch] exec_template`).
335    /// `false` means `RUNBOOK.md` / `dispatch-manifest.md` carry handoff
336    /// guidance without a per-task command recipe, and the `run` preflight
337    /// warns naming that limitation.
338    fn has_dispatch_recipes(&self) -> bool {
339        false
340    }
341
342    /// Render the harness's one-shot CLI command for a task. Angle-bracket
343    /// task paths remain for the caller to substitute.
344    fn cli_exec_command(&self, _guard: bool, _agent_model: Option<&str>) -> Option<String> {
345        None
346    }
347
348    /// **Enhancement: native conversation resume.** Whether the harness can
349    /// continue a captured native session for scripted follow-up turns.
350    fn has_conversation_resume(&self) -> bool {
351        false
352    }
353
354    /// Render one same-session follow-up command. In addition to the usual
355    /// angle-bracket task paths, `{session_arg}` and `{prompt_arg}` remain for
356    /// the conversation driver to fill with shell-quoted values.
357    fn cli_resume_command(&self, _guard: bool, _agent_model: Option<&str>) -> Option<String> {
358        None
359    }
360
361    /// **Enhancement: dispatch recipes.** The `Next:` guidance printed after
362    /// `run`: how to dispatch each task through this harness's one-shot CLI
363    /// and then ingest. Empty when no dispatch recipe is wired.
364    fn cli_next_steps(&self, _ctx: CliDispatchContext<'_>) -> String {
365        String::new()
366    }
367
368    /// **Enhancement: dispatch recipes.** Extra `dispatch-manifest.md` lines
369    /// describing this harness's dispatch recipe (command template, parallel
370    /// recipe, ingest note). `None` when the harness contributes no manifest
371    /// section.
372    fn cli_manifest_section(&self, _ctx: CliManifestContext<'_>) -> Option<Vec<String>> {
373        None
374    }
375
376    /// **Enhancement: dispatch recipes.** The post-`grade` / post-`ingest`
377    /// judge dispatch guidance for this harness. `None` leaves the generic
378    /// judge handoff in place.
379    fn cli_judge_next_steps(&self, _ctx: CliJudgeContext<'_>) -> Option<String> {
380        None
381    }
382}
383
384/// The shared (human-followed) `RUNBOOK.md` template used by every run,
385/// regardless of harness (Claude Code, Codex, OpenCode).
386pub const RUNBOOK_TEMPLATE: &str = include_str!("../../profiles/shared/runbook.md");
387
388/// Context for rendering a harness's one-shot CLI agent-dispatch guidance.
389#[derive(Debug, Clone, Copy)]
390pub struct CliDispatchContext<'a> {
391    pub guard: bool,
392    pub target_args: &'a str,
393    pub iteration: u32,
394    pub agent_model: Option<&'a str>,
395}
396
397/// Context for rendering a harness's `dispatch-manifest.md` CLI recipe.
398#[derive(Debug, Clone, Copy)]
399pub struct CliManifestContext<'a> {
400    pub guard: bool,
401    pub agent_model: Option<&'a str>,
402    /// Exclude scripted tasks from a mixed suite's one-shot recipe.
403    pub one_shot_only: bool,
404}
405
406/// Context for rendering a harness's one-shot CLI judge-dispatch guidance.
407#[derive(Debug, Clone, Copy)]
408pub struct CliJudgeContext<'a> {
409    pub guard: bool,
410    pub iteration_dir: &'a Path,
411}
412
413#[cfg(test)]
414mod tests {
415    use super::*;
416    use crate::adapters::registry::adapter_for;
417    use crate::core::Harness;
418
419    // Cross-method invariants (guard/banner lockstep, hook-matcher ⊆
420    // vocabulary, slug ↔ naming rules, …) are enforced at descriptor load
421    // time in `descriptor::validation`; only per-value pins live here.
422
423    #[test]
424    fn detect_shadowed_skills_defaults_to_none_for_harnesses_without_a_preflight() {
425        // Every built-in harness declares a shadow preflight today; a
426        // descriptor without [shadow] (the baseline BYOH shape) gets none.
427        let descriptor = crate::adapters::descriptor::load_descriptor(
428            "label = \"demo\"\nskills_dir = \".demo/skills\"\nconfig_dirs = [\".demo\"]\n",
429            "test.toml",
430        )
431        .unwrap();
432        let adapter =
433            crate::adapters::descriptor_adapter::DescriptorAdapter::from_descriptor(descriptor);
434        assert_eq!(
435            adapter.detect_shadowed_skills(Path::new("/nonexistent"), &["any-skill"]),
436            None
437        );
438    }
439
440    #[test]
441    fn has_dispatch_recipes_matches_the_readme_support_table() {
442        assert!(adapter_for(Harness::resolve("claude-code").unwrap()).has_dispatch_recipes());
443        assert!(adapter_for(Harness::resolve("codex").unwrap()).has_dispatch_recipes());
444        assert!(adapter_for(Harness::resolve("opencode").unwrap()).has_dispatch_recipes());
445    }
446
447    #[test]
448    fn skills_dir_is_harness_native() {
449        let root = Path::new("/repo");
450        assert_eq!(
451            adapter_for(Harness::resolve("claude-code").unwrap()).skills_dir(root),
452            Some(root.join(".claude").join("skills"))
453        );
454        assert_eq!(
455            adapter_for(Harness::resolve("codex").unwrap()).skills_dir(root),
456            Some(root.join(".agents").join("skills"))
457        );
458        assert_eq!(
459            adapter_for(Harness::resolve("opencode").unwrap()).skills_dir(root),
460            Some(root.join(".opencode").join("skills"))
461        );
462    }
463
464    #[test]
465    fn only_codex_and_opencode_rewrite_frontmatter() {
466        assert!(!adapter_for(Harness::resolve("claude-code").unwrap()).rewrites_frontmatter_name());
467        assert!(adapter_for(Harness::resolve("codex").unwrap()).rewrites_frontmatter_name());
468        assert!(adapter_for(Harness::resolve("opencode").unwrap()).rewrites_frontmatter_name());
469    }
470
471    #[test]
472    fn skill_invocation_signatures_are_harness_native() {
473        // (tool name, slug-carrying arg) the `__skill_invoked` meta-check
474        // matches; None routes the check to the LLM-judge fallback.
475        assert_eq!(
476            adapter_for(Harness::resolve("claude-code").unwrap()).transcript_skill_invocation(),
477            Some(("Skill".to_string(), "skill".to_string()))
478        );
479        assert_eq!(
480            adapter_for(Harness::resolve("codex").unwrap()).transcript_skill_invocation(),
481            None
482        );
483        assert_eq!(
484            adapter_for(Harness::resolve("opencode").unwrap()).transcript_skill_invocation(),
485            Some(("skill".to_string(), "name".to_string()))
486        );
487    }
488
489    #[test]
490    fn plan_mode_context_wraps_in_system_reminder_for_every_harness() {
491        for h in Harness::known() {
492            let out = adapter_for(h).render_plan_mode_context("BODY");
493            assert_eq!(out, "<system-reminder>\nBODY\n</system-reminder>");
494            assert_eq!(adapter_for(h).render_plan_mode_context("   "), "");
495            assert_eq!(
496                adapter_for(h).render_plan_mode_context("\n\n  BODY  \n\n"),
497                "<system-reminder>\nBODY\n</system-reminder>"
498            );
499        }
500    }
501
502    #[test]
503    fn run_capabilities_capture_run_option_support_by_harness() {
504        let claude = adapter_for(Harness::resolve("claude-code").unwrap()).run_capabilities();
505        assert!(claude.supports_guard);
506        assert!(claude.supports_bootstrap_with_no_stage);
507        assert!(claude.supports_stage_name_with_no_stage);
508
509        let codex = adapter_for(Harness::resolve("codex").unwrap()).run_capabilities();
510        assert!(codex.supports_guard);
511        assert!(codex.supports_bootstrap_with_no_stage);
512        assert!(!codex.supports_stage_name_with_no_stage);
513
514        let opencode = adapter_for(Harness::resolve("opencode").unwrap()).run_capabilities();
515        assert!(opencode.supports_guard);
516        assert!(opencode.supports_bootstrap_with_no_stage);
517        assert!(opencode.supports_stage_name_with_no_stage);
518    }
519
520    #[test]
521    fn guard_armed_message_is_harness_specific() {
522        // The post-arm `--guard` banner names the harness's native hook surface,
523        // so it lives behind the adapter rather than in generic run code.
524        let claude = adapter_for(Harness::resolve("claude-code").unwrap())
525            .guard_armed_message()
526            .expect("claude code has a write guard");
527        assert!(
528            claude.contains(".claude/settings.local.json"),
529            "claude banner names its hook file: {claude}"
530        );
531
532        let codex = adapter_for(Harness::resolve("codex").unwrap())
533            .guard_armed_message()
534            .expect("codex has a write guard");
535        assert!(
536            codex.contains(".codex/hooks.json"),
537            "codex banner names its hook file: {codex}"
538        );
539
540        let opencode = adapter_for(Harness::resolve("opencode").unwrap())
541            .guard_armed_message()
542            .expect("opencode has a write guard");
543        assert!(
544            opencode.contains(".opencode/plugins/slow-powers-eval-guard.js"),
545            "opencode banner names its plugin file: {opencode}"
546        );
547    }
548
549    #[test]
550    fn staged_slug_default_and_opencode_override_preserve_the_prefix() {
551        let prefix = "slow-powers-eval-";
552        assert_eq!(
553            adapter_for(Harness::resolve("claude-code").unwrap()).staged_slug(
554                prefix,
555                2,
556                "with_skill",
557                "my-skill"
558            ),
559            "slow-powers-eval-2-with_skill__my-skill"
560        );
561        assert_eq!(
562            adapter_for(Harness::resolve("opencode").unwrap()).staged_slug(
563                prefix,
564                2,
565                "with_skill",
566                "my-skill"
567            ),
568            "slow-powers-eval-2-with-skill-my-skill"
569        );
570    }
571}