theway-daemon 0.1.21

theway daemon — the single agent-runtime kernel (bin `thewayd`): harness assembly, local/sandbox tool policy, triggers/cron/session/DAG runtime, skills, MCP/LSP wiring, serving the gRPC/HTTP/MCP transports from theway-transport. Terminal UI lives in the theway-tui crate.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
//! Built-in skill catalog.
//!
//! Bundles a small, curated set of skills into the `theway` binary so users can opt them in
//! without manually checking out a skill repo into `<base>/skills` (the native install target,
//! e.g. `~/.theway/skills/`). This is the **lowest precedence** skill source — any user or
//! project skill of the same name shadows the built-in version, same as the first-wins
//! precedence in [`crate::skills::load_all`]. Among the user roots, `<base>/skills` is scanned
//! before the remaining `$HOME` roots (issue #66), so an installed skill shadows a same-name
//! copy there too.
//!
//! **Default behavior is OFF**: a built-in skill is included in the harness skill catalog
//! only when the user explicitly enables it via:
//!
//! - the `--builtin-skill <name>` CLI flag (repeatable, one-time enable for that run)
//! - `~/.theway/config.toml`: `[builtin_skills] enabled = [...]` (persistent enable)
//!
//! Both inputs are unioned + de-duplicated. Unknown names from the CLI flag are a hard error
//! (the user typed a name and we cannot honor it). Unknown names from the config file are a
//! soft startup diagnostic (the config may have drifted from the binary's bundled set, but we
//! do not lock the user out — known names still take effect, unknown names are simply
//! skipped). Either way, an unknown name **never** silently enables anything.
//!
//! See c4pt0r/theway#32 for the spec.

use std::collections::BTreeSet;
use theway_core::{Skill, SkillSource};

/// Raw markdown of each built-in skill, vendored verbatim under
/// `crates/harness/skills/<name>/SKILL.md` so the upstream sync path stays a plain
/// file copy. The frontmatter is preserved; `content_after_frontmatter` strips it at runtime.
struct BuiltinSpec {
    /// Stable lowercase-kebab name. Must match the frontmatter `name:` value.
    name: &'static str,
    /// Short description shown in `/skills` and the system-prompt catalog. Must match the
    /// upstream frontmatter `description:` value so the catalog text matches what users see
    /// in the source repo.
    description: &'static str,
    /// Full SKILL.md content including frontmatter. Bundled at compile time.
    raw_markdown: &'static str,
}

/// All built-in skills bundled with this `theway` build. Adding a new one means: vendor the
/// SKILL.md under `crates/harness/skills/<name>/`, then add an entry here.
const BUILTINS: &[BuiltinSpec] = &[BuiltinSpec {
    name: "karpathy-guidelines",
    description: "Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.",
    raw_markdown: include_str!("../skills/karpathy-guidelines/SKILL.md"),
}];

/// List every built-in name in stable alphabetical order. Used by error/diagnostic messages
/// so the `Available: ...` list is reproducible across runs.
pub fn available_builtin_names() -> Vec<&'static str> {
    let mut out: Vec<&'static str> = BUILTINS.iter().map(|b| b.name).collect();
    out.sort_unstable();
    out
}

/// Outcome of resolving the user's requested set of built-in skills.
#[derive(Debug)]
pub struct ResolvedBuiltins {
    /// Skills to fold into the harness skill catalog. Empty when no built-in was enabled.
    pub skills: Vec<Skill>,
    /// Soft diagnostic strings to print at startup (e.g. unknown names found in
    /// `~/.theway/config.toml`). Each string is a complete user-readable line; the caller emits
    /// them however it normally surfaces non-fatal startup notices.
    pub diagnostics: Vec<String>,
}

/// Build the union of CLI-requested + config-requested built-in skills.
///
/// `cli_requested` is treated as authoritative: an unknown name is an error returned to the
/// caller (the CLI surface is expected to hard-fail with a non-zero exit). `config_requested`
/// is treated permissively: unknown names produce a diagnostic line but do not fail startup.
/// Known names from either source are unioned and de-duplicated; the same name appearing in
/// both, or twice in the CLI list, still produces exactly one catalog entry.
pub fn resolve_builtins(
    cli_requested: &[String],
    config_requested: &[String],
) -> Result<ResolvedBuiltins, UnknownBuiltinError> {
    // CLI path: hard-fail on any unknown name. We collect all unknowns and report them
    // together so the user sees the full list, not a one-at-a-time game of whack-a-mole.
    let known: BTreeSet<&'static str> = available_builtin_names().into_iter().collect();
    let mut unknown_cli: Vec<String> = Vec::new();
    for name in cli_requested {
        if !known.contains(name.as_str()) {
            unknown_cli.push(name.clone());
        }
    }
    if !unknown_cli.is_empty() {
        unknown_cli.sort();
        unknown_cli.dedup();
        return Err(UnknownBuiltinError {
            unknown: unknown_cli,
            available: available_builtin_names()
                .into_iter()
                .map(str::to_string)
                .collect(),
        });
    }

    // Config path: collect unknowns into a diagnostic, but do not block startup.
    let mut diagnostics: Vec<String> = Vec::new();
    let mut unknown_config: Vec<String> = Vec::new();
    for name in config_requested {
        if !known.contains(name.as_str()) {
            unknown_config.push(name.clone());
        }
    }
    if !unknown_config.is_empty() {
        unknown_config.sort();
        unknown_config.dedup();
        diagnostics.push(format!(
            "config: ignoring unknown built-in skill(s) in `[builtin_skills] enabled`: {}. Available: {}.",
            unknown_config.join(", "),
            available_builtin_names().join(", ")
        ));
    }

    // Union + dedup the known names from both sources.
    let mut enabled: BTreeSet<&'static str> = BTreeSet::new();
    for name in cli_requested.iter().chain(config_requested.iter()) {
        if let Some(known_name) = known.get(name.as_str()) {
            enabled.insert(known_name);
        }
    }

    // Build Skill structs in stable alphabetical order so the system prompt catalog is
    // reproducible across runs (matches the existing `format_skills_for_system_prompt` sort).
    let mut skills: Vec<Skill> = Vec::with_capacity(enabled.len());
    for name in enabled {
        let spec = BUILTINS
            .iter()
            .find(|b| b.name == name)
            .expect("name validated against known set above");
        skills.push(spec_to_skill(spec));
    }

    Ok(ResolvedBuiltins {
        skills,
        diagnostics,
    })
}

fn spec_to_skill(spec: &BuiltinSpec) -> Skill {
    Skill {
        name: spec.name.to_string(),
        description: spec.description.to_string(),
        // Synthetic path used in `/skills` listings and audit. Matches the format chosen for
        // built-in tier so users can tell where a skill came from at a glance.
        file_path: format!("<builtin>/{}/SKILL.md", spec.name),
        content: strip_frontmatter(spec.raw_markdown).to_string(),
        disable_model_invocation: false,
        source: SkillSource::Builtin,
    }
}

/// Return the body of a SKILL.md after stripping the leading YAML frontmatter block, if any.
/// Mirrors the behavior the on-disk loader applies to a real SKILL.md.
fn strip_frontmatter(content: &str) -> &str {
    let trimmed = content.trim_start_matches('\u{feff}');
    let Some(without_open) = trimmed.strip_prefix("---") else {
        return content;
    };
    // Allow the opening `---` to be followed by either a newline (most files) or any
    // whitespace before the newline (be lenient — same posture as the loader's parser).
    let after_open = match without_open.find('\n') {
        Some(i) => &without_open[i + 1..],
        None => return content,
    };
    // Find the closing `---` on its own line.
    let mut search_from = 0usize;
    while let Some(pos) = after_open[search_from..].find("\n---") {
        let absolute = search_from + pos + 1; // skip the leading '\n'
        let after_close = &after_open[absolute + 3..];
        // Closing line ends with newline or EOF.
        if let Some(rest) = after_close.strip_prefix('\n') {
            return rest.trim_start_matches('\n');
        }
        if after_close.is_empty() {
            return "";
        }
        // The "---" we found has trailing text; keep scanning.
        search_from = absolute + 3;
    }
    // No closing marker — return the original content rather than guess.
    content
}

/// Merge the resolved built-in skills with the user/project skills the multi-root loader
/// returned. Same-name precedence is **user/project wins over built-in** (built-in is
/// the lowest tier per #32). The returned `Vec<Skill>` preserves built-in skills first, then
/// any user/project skills not already shadowing a built-in. Repeated names within
/// `user_project` follow the loader's first-wins root order (`--skills-dir` extras > project
/// roots > `<base>/skills` install target > remaining `$HOME` user roots, issue #66) and
/// arrive here already collapsed.
///
/// This is extracted out of `main.rs` so the wiring path can be unit-tested without spinning
/// up the full binary (per @CLI-TUI-Dev-Lead's review on PR #34).
pub fn merge_with_user_project(mut builtins: Vec<Skill>, user_project: &[Skill]) -> Vec<Skill> {
    for skill in user_project.iter() {
        if let Some(slot) = builtins.iter_mut().find(|s| s.name == skill.name) {
            // User / project skill of the same name shadows the built-in.
            *slot = skill.clone();
        } else {
            builtins.push(skill.clone());
        }
    }
    builtins
}

/// Parse the contents of `~/.theway/config.toml` and extract the
/// `[builtin_skills] enabled = [...]` list. Missing section / missing key / parse failure all
/// degrade to an empty list — the soft fail-closed posture from #32: the caller treats
/// unknown names as a startup diagnostic, but a malformed config never prevents `theway` from
/// running at all.
///
/// The parser lives in transport (`theway_transport::config::parse_builtin_skills_config`),
/// re-imported here so this module's unit tests keep the unqualified call sites.
#[cfg(test)]
use theway_transport::config::parse_builtin_skills_config;

/// Error returned when the CLI enabled a built-in skill name that this binary does not
/// recognise. The caller is expected to print the message and exit with a non-zero status
/// (hard fail, per #32's CLI-side acceptance).
#[derive(Debug)]
pub struct UnknownBuiltinError {
    pub unknown: Vec<String>,
    pub available: Vec<String>,
}

impl std::fmt::Display for UnknownBuiltinError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "unknown built-in skill(s) requested via --builtin-skill: {}. Available: {}.",
            self.unknown.join(", "),
            self.available.join(", ")
        )
    }
}

impl std::error::Error for UnknownBuiltinError {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn available_names_is_sorted_and_contains_karpathy() {
        let names = available_builtin_names();
        let mut sorted = names.clone();
        sorted.sort_unstable();
        assert_eq!(
            names, sorted,
            "available names must be alphabetically sorted"
        );
        assert!(names.contains(&"karpathy-guidelines"));
    }

    #[test]
    fn no_request_returns_empty_no_diagnostics() {
        let resolved = resolve_builtins(&[], &[]).expect("empty inputs are always OK");
        assert!(resolved.skills.is_empty());
        assert!(resolved.diagnostics.is_empty());
    }

    #[test]
    fn cli_known_name_enables_skill_with_stripped_body() {
        let resolved =
            resolve_builtins(&["karpathy-guidelines".to_string()], &[]).expect("known name");
        assert_eq!(resolved.skills.len(), 1);
        let s = &resolved.skills[0];
        assert_eq!(s.name, "karpathy-guidelines");
        assert!(s.description.starts_with("Behavioral guidelines"));
        assert_eq!(s.file_path, "<builtin>/karpathy-guidelines/SKILL.md");
        // Frontmatter is stripped — body starts with the H1 header.
        assert!(
            s.content.starts_with("# Karpathy Guidelines"),
            "expected body to start with the H1 header, got: {:?}",
            &s.content[..s.content.len().min(80)]
        );
        // No frontmatter delimiter left in the body.
        assert!(!s.content.starts_with("---"));
        assert!(!s.content.contains("\nlicense: MIT"));
        // Sanity: real guideline text is there.
        assert!(s.content.contains("Think Before Coding"));
        // disable_model_invocation defaults to false (frontmatter has no flag).
        assert!(!s.disable_model_invocation);
    }

    #[test]
    fn cli_unknown_name_hard_fails_with_available_list() {
        let err = resolve_builtins(&["nonexistent-skill".to_string()], &[])
            .expect_err("unknown CLI name must hard fail");
        assert_eq!(err.unknown, vec!["nonexistent-skill".to_string()]);
        assert!(err.available.contains(&"karpathy-guidelines".to_string()));
        // Sorted available list — assert order is stable.
        let mut sorted_available = err.available.clone();
        sorted_available.sort();
        assert_eq!(err.available, sorted_available);
    }

    #[test]
    fn cli_mixes_known_and_unknown_reports_all_unknown_at_once() {
        let err = resolve_builtins(
            &[
                "karpathy-guidelines".to_string(),
                "missing-a".to_string(),
                "missing-b".to_string(),
            ],
            &[],
        )
        .expect_err("any unknown CLI name must hard fail");
        // Whack-a-mole avoidance — both unknowns surface in one error.
        assert!(err.unknown.contains(&"missing-a".to_string()));
        assert!(err.unknown.contains(&"missing-b".to_string()));
        assert_eq!(err.unknown.len(), 2);
    }

    #[test]
    fn config_unknown_name_is_soft_warning_not_fail() {
        let resolved = resolve_builtins(&[], &["nonexistent-skill".to_string()])
            .expect("unknown config name must NOT hard fail");
        assert!(
            resolved.skills.is_empty(),
            "unknown name must not enable anything"
        );
        assert_eq!(resolved.diagnostics.len(), 1);
        let diag = &resolved.diagnostics[0];
        assert!(diag.contains("nonexistent-skill"));
        assert!(diag.contains("Available: karpathy-guidelines"));
    }

    #[test]
    fn config_mixes_known_and_unknown_keeps_known_skips_unknown() {
        let resolved = resolve_builtins(
            &[],
            &["karpathy-guidelines".to_string(), "missing".to_string()],
        )
        .expect("config soft path must not fail on unknown");
        assert_eq!(resolved.skills.len(), 1);
        assert_eq!(resolved.skills[0].name, "karpathy-guidelines");
        assert_eq!(resolved.diagnostics.len(), 1);
        assert!(resolved.diagnostics[0].contains("missing"));
    }

    #[test]
    fn cli_and_config_same_name_does_not_duplicate_catalog_entry() {
        let resolved = resolve_builtins(
            &["karpathy-guidelines".to_string()],
            &["karpathy-guidelines".to_string()],
        )
        .expect("known on both sides is fine");
        assert_eq!(
            resolved.skills.len(),
            1,
            "union should dedup the same name across CLI + config"
        );
        assert!(resolved.diagnostics.is_empty());
    }

    #[test]
    fn cli_repeated_same_name_does_not_duplicate_catalog_entry() {
        let resolved = resolve_builtins(
            &[
                "karpathy-guidelines".to_string(),
                "karpathy-guidelines".to_string(),
            ],
            &[],
        )
        .expect("repeated --builtin-skill should be idempotent");
        assert_eq!(resolved.skills.len(), 1);
    }

    fn fake_skill(name: &str, file_path: &str) -> Skill {
        Skill {
            name: name.into(),
            description: format!("desc for {name}"),
            file_path: file_path.into(),
            content: format!("body of {name}"),
            disable_model_invocation: false,
            source: SkillSource::User,
        }
    }

    #[test]
    fn merge_no_user_project_returns_builtins_unchanged() {
        let builtins = vec![fake_skill(
            "karpathy-guidelines",
            "<builtin>/karpathy-guidelines/SKILL.md",
        )];
        let merged = merge_with_user_project(builtins.clone(), &[]);
        assert_eq!(merged.len(), 1);
        assert_eq!(merged[0].name, "karpathy-guidelines");
        assert_eq!(
            merged[0].file_path,
            "<builtin>/karpathy-guidelines/SKILL.md"
        );
    }

    #[test]
    fn merge_user_project_skill_shadows_builtin_same_name() {
        // Same-name precedence acceptance from #32: user / project skill of the same name
        // wins over the built-in. The combined catalog still has exactly one entry; the
        // built-in entry is replaced in place, not appended.
        let builtins = vec![fake_skill(
            "karpathy-guidelines",
            "<builtin>/karpathy-guidelines/SKILL.md",
        )];
        let user_project = vec![fake_skill(
            "karpathy-guidelines",
            "/home/me/.theway/skills/karpathy-guidelines/SKILL.md",
        )];
        let merged = merge_with_user_project(builtins, &user_project);
        assert_eq!(merged.len(), 1, "same name must collapse to one entry");
        assert_eq!(merged[0].name, "karpathy-guidelines");
        assert_eq!(
            merged[0].file_path, "/home/me/.theway/skills/karpathy-guidelines/SKILL.md",
            "user / project entry must shadow the built-in"
        );
    }

    #[test]
    fn merge_unrelated_user_project_skills_appended_after_builtins() {
        let builtins = vec![fake_skill(
            "karpathy-guidelines",
            "<builtin>/karpathy-guidelines/SKILL.md",
        )];
        let user_project = vec![fake_skill(
            "my-personal-skill",
            "/home/me/.theway/skills/my-personal-skill/SKILL.md",
        )];
        let merged = merge_with_user_project(builtins, &user_project);
        assert_eq!(merged.len(), 2);
        assert_eq!(merged[0].name, "karpathy-guidelines");
        assert_eq!(merged[1].name, "my-personal-skill");
    }

    #[test]
    fn merge_handles_empty_builtins_with_user_project() {
        let user_project = vec![fake_skill(
            "my-personal-skill",
            "/home/me/.theway/skills/my-personal-skill/SKILL.md",
        )];
        let merged = merge_with_user_project(Vec::new(), &user_project);
        assert_eq!(merged.len(), 1);
        assert_eq!(merged[0].name, "my-personal-skill");
    }

    #[test]
    fn parse_config_extracts_enabled_list() {
        let text = r#"
[builtin_skills]
enabled = ["karpathy-guidelines", "future-other-skill"]
"#;
        let enabled = parse_builtin_skills_config(text);
        assert_eq!(
            enabled,
            vec![
                "karpathy-guidelines".to_string(),
                "future-other-skill".to_string()
            ]
        );
    }

    #[test]
    fn parse_config_missing_section_is_empty_list() {
        let text = r#"
[some_other_section]
key = "value"
"#;
        let enabled = parse_builtin_skills_config(text);
        assert!(enabled.is_empty());
    }

    #[test]
    fn parse_config_missing_enabled_key_is_empty_list() {
        let text = r#"
[builtin_skills]
"#;
        let enabled = parse_builtin_skills_config(text);
        assert!(enabled.is_empty());
    }

    #[test]
    fn parse_config_malformed_toml_degrades_to_empty_not_panic() {
        // Soft fail-closed: a typo in the config never blocks startup.
        let text = "this is not valid toml [ [ [";
        let enabled = parse_builtin_skills_config(text);
        assert!(enabled.is_empty());
    }

    #[test]
    fn parse_config_empty_string_is_empty_list() {
        let enabled = parse_builtin_skills_config("");
        assert!(enabled.is_empty());
    }

    #[test]
    fn strip_frontmatter_preserves_content_without_frontmatter() {
        let raw = "# Just a body\n";
        assert_eq!(strip_frontmatter(raw), raw);
    }

    #[test]
    fn strip_frontmatter_returns_original_when_opening_has_no_newline() {
        let raw = "--- not frontmatter";
        assert_eq!(strip_frontmatter(raw), raw);
    }

    #[test]
    fn strip_frontmatter_returns_original_when_no_closing_marker() {
        let raw = "---\nname: x\nbody without close";
        assert_eq!(strip_frontmatter(raw), raw);
    }

    #[test]
    fn strip_frontmatter_handles_closing_line_with_trailing_text_and_eof() {
        let raw = "---\nname: x\n--- trailing\nbody";
        assert_eq!(strip_frontmatter(raw), raw);

        let raw_eof = "---\nname: x\n---";
        assert_eq!(strip_frontmatter(raw_eof), "");
    }

    #[test]
    fn strip_frontmatter_skips_bom() {
        let raw = "\u{feff}---\nname: x\n---\nbody";
        assert_eq!(strip_frontmatter(raw), "body");
    }

    #[test]
    fn vendored_skill_md_frontmatter_matches_hardcoded_metadata() {
        // Self-check: if someone updates the vendored SKILL.md and forgets to update the
        // hardcoded BuiltinSpec, this test catches the drift. Match against the description
        // text we surface in /skills + the system prompt.
        let raw = BUILTINS
            .iter()
            .find(|b| b.name == "karpathy-guidelines")
            .unwrap();
        assert!(raw.raw_markdown.contains("name: karpathy-guidelines"));
        assert!(
            raw.raw_markdown.contains(raw.description),
            "vendored SKILL.md description must byte-match the hardcoded BuiltinSpec.description"
        );
    }
}