Skip to main content

agentlink_domain/
layout.rs

1//! The canonical layout: the single source of truth on disk.
2//!
3//! The layout is deliberately *not* an agentlink invention. Every path here is
4//! one that a significant share of the ecosystem already reads natively:
5//!
6//! | Canonical        | Read natively by                                     |
7//! |------------------|------------------------------------------------------|
8//! | `AGENTS.md`      | Codex, Cursor, Copilot, `OpenCode`, Antigravity, …   |
9//! | `.agents/skills` | Antigravity (IDE/CLI/AGY), Codex                     |
10//!
11//! Choosing the layout most tools already understand is what keeps the number of
12//! [`Strategy::Native`](crate::model::Strategy::Native) verdicts high, and every
13//! native verdict is work that never has to happen. See
14//! `docs/adr/0002-canonical-layout.md`.
15
16use crate::model::{NodeKind, ResourceKind};
17use crate::path::RelPath;
18
19/// Directory holding agentlink's own state, relative to the workspace root.
20pub const STATE_DIR: &str = ".agentlink";
21
22/// User-editable configuration.
23pub const CONFIG_FILE: &str = ".agentlink/config.toml";
24
25/// Record of everything agentlink has materialised.
26pub const LOCK_FILE: &str = ".agentlink/lock.toml";
27
28/// Directory scanned for workspace-local provider manifests.
29pub const LOCAL_PROVIDERS_DIR: &str = ".agentlink/providers";
30
31/// The canonical layout of a workspace.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Layout {
34    instructions: RelPath,
35    skills: RelPath,
36}
37
38impl Default for Layout {
39    fn default() -> Self {
40        Self {
41            instructions: RelPath::new("AGENTS.md").expect("valid default"),
42            skills: RelPath::new(".agents/skills").expect("valid default"),
43        }
44    }
45}
46
47impl Layout {
48    /// The canonical path for a resource kind.
49    pub fn canonical(&self, kind: ResourceKind) -> &RelPath {
50        match kind {
51            ResourceKind::Instructions => &self.instructions,
52            ResourceKind::Skills => &self.skills,
53        }
54    }
55
56    /// The node kind of a canonical resource.
57    pub fn node(&self, kind: ResourceKind) -> NodeKind {
58        kind.node()
59    }
60
61    /// Paths that `init` should create so the workspace has a source of truth.
62    pub fn seeds(&self) -> impl Iterator<Item = (ResourceKind, &RelPath)> {
63        ResourceKind::ALL
64            .iter()
65            .map(|&kind| (kind, self.canonical(kind)))
66    }
67}
68
69/// The starter `AGENTS.md` written by `agentlink init` when none exists.
70///
71/// Kept intentionally short: this file is read by every agent on every task, so
72/// bloat here is a tax paid on every single request.
73pub const AGENTS_MD_TEMPLATE: &str = "\
74# AGENTS.md
75
76Shared instructions for every AI coding agent working in this repository.
77Managed with [agentlink](https://github.com/fialhosoft/agentlink) — edit this
78file from any agent and the change is visible to all of them.
79
80## Project
81
82<!-- What this project is, in two or three sentences. -->
83
84## Commands
85
86<!-- Build, test and lint commands an agent should use. -->
87
88## Conventions
89
90<!-- Code style, patterns to follow, and anything that is off limits. -->
91";
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn canonical_paths_match_the_ecosystem_standards() {
99        let layout = Layout::default();
100        assert_eq!(
101            layout.canonical(ResourceKind::Instructions).as_str(),
102            "AGENTS.md"
103        );
104        assert_eq!(
105            layout.canonical(ResourceKind::Skills).as_str(),
106            ".agents/skills"
107        );
108    }
109
110    #[test]
111    fn every_resource_kind_has_a_seed() {
112        let layout = Layout::default();
113        assert_eq!(layout.seeds().count(), ResourceKind::ALL.len());
114    }
115}