use crate::hook::Field;
use crate::render::Server;
use anyhow::{Context, Result};
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
pub version: String,
#[serde(default, rename = "entry")]
pub entries: Vec<Entry>,
#[serde(default)]
pub rejected: Vec<Rejected>,
#[serde(skip)]
pub path: Option<PathBuf>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Entry {
pub name: String,
pub kind: Kind,
pub feature: String,
pub since: String,
pub because: String,
pub remove: String,
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub measured: Vec<Measured>,
#[serde(default)]
pub instead_of: Vec<Alternative>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Kind {
Mcp,
Hook,
Rules,
}
impl Kind {
pub fn capability(&self) -> crate::adapter::Capability {
match self {
Self::Mcp => crate::adapter::Capability::Mcp,
Self::Hook => crate::adapter::Capability::Hooks,
Self::Rules => crate::adapter::Capability::Rules,
}
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Measured {
pub what: String,
pub value: String,
pub how: String,
pub on: String,
}
pub fn parse_ym(s: &str) -> Option<(u32, u32)> {
let mut parts = s.split(['.', '-']);
let year: u32 = parts.next()?.parse().ok()?;
let month: u32 = parts.next()?.parse().ok()?;
(year >= 2000 && (1..=12).contains(&month)).then_some((year, month))
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Alternative {
pub name: String,
pub why: String,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Rejected {
pub name: String,
pub considered: String,
pub because: String,
}
impl Manifest {
pub fn load_dir(dir: &Path) -> Result<Self> {
let mut newest: Option<((u32, u32), PathBuf, Self)> = None;
for entry in std::fs::read_dir(dir)
.with_context(|| format!("reading {}", dir.display()))?
.flatten()
{
let path = entry.path();
if !path.extension().is_some_and(|x| x == "toml") {
continue;
}
let raw = std::fs::read_to_string(&path)
.with_context(|| format!("reading {}", path.display()))?;
let manifest: Self = toml::from_str(&raw).with_context(|| {
format!(
"parsing {} — if it was seeded by an older omh, `omh init` refreshes it",
path.display()
)
})?;
let Some(version) = parse_ym(&manifest.version) else {
continue;
};
if newest.as_ref().is_none_or(|(best, _, _)| version > *best) {
newest = Some((version, path, manifest));
}
}
let (_, path, mut manifest) = newest.with_context(|| {
format!(
"no usable base manifest in {} — run `omh init`",
dir.display()
)
})?;
if manifest.entries.is_empty() {
anyhow::bail!("{} declares no base-set entries", path.display());
}
manifest.path = Some(path);
Ok(manifest)
}
pub fn source(&self) -> String {
match &self.path {
Some(p) => format!("{} · {}", p.display(), self.version),
None => format!("(unsaved) · {}", self.version),
}
}
pub fn servers(&self) -> BTreeMap<String, Server> {
self.entries
.iter()
.filter(|e| e.kind == Kind::Mcp)
.filter_map(|e| {
Some((
e.name.clone(),
Server {
command: e.command.clone()?,
args: e.args.clone(),
env: BTreeMap::new(),
},
))
})
.collect()
}
pub fn rationale(&self) -> Vec<(&str, &str)> {
self.entries
.iter()
.filter(|e| e.kind == Kind::Mcp)
.map(|e| (e.name.as_str(), e.because.as_str()))
.collect()
}
pub fn entry(&self, name: &str) -> Option<&Entry> {
self.entries.iter().find(|e| e.name == name)
}
pub fn owns(&self) -> crate::selection::Owned {
let mut out = crate::selection::Owned::new();
for entry in &self.entries {
out.entry(entry.kind.capability())
.or_default()
.insert(entry.name.clone(), entry.feature.clone());
}
out
}
pub fn rejection(&self, name: &str) -> Option<&Rejected> {
self.rejected.iter().find(|r| r.name == name)
}
}
pub const GRAPH_CACHE: &str = "/home/agent/.cache/codebase-memory-mcp";
pub const GRAPH_VERSION: &str = "0.9.0";
pub const GRAPH_UI_PORT: u16 = 9749;
pub const GRAPH_UI_INTERNAL: u16 = 9748;
pub const GRAPH_BIN: &str = "codebase-memory-mcp";
pub fn ui_container(repo: &str) -> String {
format!("omh-graph-{repo}")
}
pub fn ui_port(container: &str) -> u16 {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
container.hash(&mut h);
"graph-ui".hash(&mut h);
const LOW: u32 = 49152;
(LOW + (h.finish() % (65535 - LOW) as u64) as u32) as u16
}
pub fn graph_install() -> String {
format!(
"set -eu; \
ARCH=${{TARGETARCH:-$(dpkg --print-architecture)}}; \
A=codebase-memory-mcp-ui-linux-$ARCH-portable.tar.gz; \
B=https://github.com/DeusData/codebase-memory-mcp/releases/download/v{GRAPH_VERSION}; \
cd /tmp && curl -sSLO \"$B/$A\" && curl -sSLO \"$B/checksums.txt\" && \
grep \" $A$\" checksums.txt | sha256sum -c - && \
tar xzf \"$A\" && \
install -m 0755 \"$(find /tmp -maxdepth 2 -name {GRAPH_BIN} -type f | head -1)\" \
/usr/local/bin/{GRAPH_BIN} && \
rm -rf /tmp/*"
)
}
pub fn ui_command(port: u16) -> String {
format!(
"sleep infinity | {GRAPH_BIN} --ui=true --port={GRAPH_UI_INTERNAL} & \
socat TCP-LISTEN:{port},fork,reuseaddr TCP:127.0.0.1:{GRAPH_UI_INTERNAL}"
)
}
pub fn ui_run_args(image: &str, container: &str, cache_volume: &str, port: u16) -> Vec<String> {
vec![
"run".into(),
"-d".into(),
"--name".into(),
container.into(),
"-p".into(),
format!("127.0.0.1:{port}:{GRAPH_UI_PORT}"),
"-v".into(),
format!("{cache_volume}:{GRAPH_CACHE}"),
image.into(),
"sh".into(),
"-c".into(),
ui_command(GRAPH_UI_PORT),
]
}
pub fn drop_graph_command(project: &str) -> Vec<String> {
vec![
"sh".into(),
"-c".into(),
format!("{GRAPH_BIN} cli delete_project --project '{project}' >/dev/null 2>&1 || true"),
]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Hook {
pub name: &'static str,
pub hook: crate::hook::Hook,
}
pub const PROJECT_ENV: &str = "OMH_GRAPH_PROJECT";
pub fn project_name(repo: &str, session: &str) -> String {
format!("{repo}-{session}")
}
const GREP_NUDGE: [&str; 3] = [
"This repo has a code graph: project ",
". For structural questions — where is X defined, what calls Y, what does \
this module depend on — search_graph --project ",
" answers in one call. Grep is right for literal text.",
];
#[cfg(test)]
pub fn grep_nudge(project: &str) -> String {
format!(
"{}{project}{}{project}{}",
GREP_NUDGE[0], GREP_NUDGE[1], GREP_NUDGE[2]
)
}
pub fn hooks() -> Vec<Hook> {
use crate::hook::{Action, Event, Hook as Canonical, Tool};
const SOURCE: &str = "*.rs|*.ts|*.tsx|*.js|*.jsx|*.py|*.go|*.java|*.rb|*.php|*.c|*.h|*.cc|\
*.cpp|*.hpp|*.cs|*.swift|*.kt|*.scala";
vec![
Hook {
name: "graph-refresh",
hook: Canonical {
on: Event::TurnEnd,
tools: vec![],
when: None,
action: Action::Run(format!(
"{GRAPH_BIN} cli index_repository --repo-path /work \
--name \"${PROJECT_ENV}\" --mode fast >/dev/null 2>&1 || true"
)),
},
},
Hook {
name: "graph-orient",
hook: Canonical {
on: Event::SessionStart,
tools: vec![],
when: Some(format!("[ -n \"${}\" ]", crate::hook::CAPTURE_VAR)),
action: Action::Inject {
capture: Some(format!(
"{GRAPH_BIN} cli get_architecture --project \"${PROJECT_ENV}\" \
--aspects layers --aspects packages --aspects boundaries \
--aspects entry_points 2>/dev/null | tail -1"
)),
text: format!(
"Code graph for project ${PROJECT_ENV} — modules, layers, boundaries \
and entry points. Query it with search_graph/trace_path/get_code_snippet \
rather than exploring by hand:\n${}",
crate::hook::CAPTURE_VAR
),
},
},
},
Hook {
name: "git-unavailable",
hook: Canonical {
on: Event::BeforeTool,
tools: vec![Tool::Shell],
when: Some(format!(
"case \"${}\" in \
git\\ *|git) ;; \
*[\\;\\&\\|\\(]*git\\ *|*[[:blank:]]git\\ *) ;; \
*\"\n\"git\\ *) ;; \
*) false ;; esac",
Field::ToolCommand.var()
)),
action: Action::Refuse {
text: GIT_ABSENT.to_string(),
},
},
},
Hook {
name: "graph-first",
hook: Canonical {
on: Event::BeforeTool,
tools: vec![Tool::Search],
when: None,
action: Action::Inject {
capture: None,
text: format!(
"{}${PROJECT_ENV}{}${PROJECT_ENV}{}",
GREP_NUDGE[0], GREP_NUDGE[1], GREP_NUDGE[2]
),
},
},
},
Hook {
name: "graph-read",
hook: Canonical {
on: Event::BeforeTool,
tools: vec![Tool::Read],
when: Some(format!(
"case \"${f}\" in {SOURCE}) ;; *) false ;; esac && \
[ -f \"${f}\" ] && [ \"$(wc -c < \"${f}\")\" -gt 8000 ]",
f = Field::ToolFile.var()
)),
action: Action::Inject {
capture: None,
text: format!(
"${f} is large. For one symbol rather than the whole file: \
get_code_snippet --project ${PROJECT_ENV} --qualified-name <name>, \
and search_graph finds the name.",
f = Field::ToolFile.var()
),
},
},
},
]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Section {
pub name: &'static str,
pub body: String,
}
pub const GIT_ABSENT: &str = "git does not work in this session, by design and not by fault. \
The worktree's .git is a pointer at an admin directory on the host, which omh does not \
mount — so every git command fails with `fatal: not a git repository`. Do not try to \
repair it: `git init` refuses for the same reason, and re-cloning would only give you a \
second repository nobody is reviewing. Nothing here is broken and nothing is lost. Your \
work is already visible outside the sandbox, where the person you are working with \
reviews it with `omh s diff`, commits it with `omh s commit`, and pushes it with \
`omh s push`. Say that rather than offering to commit yourself.";
pub fn sections() -> Vec<Section> {
vec![
Section {
name: "graph-rules",
body: "## Code graph\n\n\
This repo is indexed as a graph, refreshed after every turn. Prefer it over\n\
reading or grepping files when the question is structural:\n\n\
- `search_graph` — where is X defined, what is named like Y\n\
- `trace_path` — how does A reach B\n\
- `get_architecture` — what the modules are and how they depend on each other\n\
- `get_code_snippet` — read one symbol instead of a whole file\n\n\
Grep is still right for literal text: a string, a config value, a TODO.\n\n\
**Use the project named by `$OMH_GRAPH_PROJECT`.** Other sessions of this\n\
repo have their own graphs in the same store; querying one of those answers\n\
confidently about code that is not in this worktree.\n"
.into(),
},
Section {
name: "git-rules",
body: format!("## Git\n\n{GIT_ABSENT}\n"),
},
Section {
name: "memory-rules",
body: format!(
"## Which graph to ask\n\n\
There are two, and they do not overlap:\n\n\
- **the code graph** knows **what the code is** — where a symbol lives, how\n \
one module reaches another. Re-derived from the code every turn, so it is\n \
never out of date and never needs to be told anything.\n\
- **`recall`** knows **why** it is that way — what was tried and failed, what\n \
turned out not to work, what surprised somebody. None of that is in the\n \
code, so no amount of reading will recover it.\n\n\
A *where* or *what* question goes to the code graph. A *why*, *is this safe*,\n\
or *has this been tried* question goes to `recall`. When you are about to\n\
assume how something here behaves, ask `recall` first — that is exactly the\n\
assumption somebody already got wrong once.\n\n\
They compose: find the code with the code graph, then ask `recall` what is\n\
known about it before changing it.\n\n\
{}",
note_taking()
),
},
]
}
fn note_taking() -> String {
format!(
"## Memory\n\n\
When something surprises you — you expected one thing and the repo did\n\
another — record it. Not what you did; what you were wrong about.\n\n\
Write a Markdown file into `{}/`, named after the\n\
observation, in this shape:\n\n\
```markdown\n\
---\n\
key: <the filename, without .md>\n\
type: surprise\n\
source: session $OMH_SESSION, <this harness>\n\
recorded: <YYYY-MM-DD, the day it happened>\n\
---\n\n\
# One line naming the surprise\n\n\
## Expected\n\n\
## Observed\n\n\
## Evidence\n\n\
## Answers\n\n\
- <the question somebody would later ask to find this>\n\n\
## Related\n\n\
- [[another-notes-key]]\n\
```\n\n\
**Answers** is what makes the note findable later, and only you know it:\n\
write the question you would have asked five minutes ago, in the words you\n\
would have used. A note nobody can find is a note nobody wrote.\n\n\
Store uncertainty rather than false precision, and date by when the thing\n\
happened rather than when you mentioned it. If you have nothing to put\n\
under **Expected**, there is nothing here worth recording.\n\n\
Rename a note by rewriting its `key` and its filename together — never\n\
one without the other.\n",
crate::memory::GUEST_LOCAL_NOTES,
)
}
#[cfg_attr(test, derive(Default))]
#[derive(Debug, Clone)]
pub struct Own {
pub hooks: Vec<Hook>,
pub sections: Vec<Section>,
pub reserved: BTreeSet<String>,
}
pub fn own(
manifest: &Manifest,
off: &BTreeSet<String>,
installed: &BTreeSet<String>,
) -> Result<Own> {
let gone: BTreeSet<&str> = manifest
.entries
.iter()
.filter(|e| e.kind == Kind::Mcp)
.fold(BTreeMap::<&str, bool>::new(), |mut acc, e| {
let present = installed.contains(&e.name);
*acc.entry(e.feature.as_str()).or_insert(false) |= present;
acc
})
.into_iter()
.filter(|(_, present)| !present)
.map(|(feature, _)| feature)
.collect();
let on = |name: &str| -> Result<bool> {
let entry = manifest.entry(name).with_context(|| {
format!(
"this omh ships `{name}` and {} describes no entry for it — the binary and the manifest disagree about the base set. `omh init` refreshes the bundled manifest.",
manifest.source()
)
})?;
Ok(!off.contains(&entry.feature) && !gone.contains(entry.feature.as_str()))
};
let mut own = Own {
hooks: Vec::new(),
sections: Vec::new(),
reserved: manifest
.entries
.iter()
.filter(|e| e.kind == Kind::Hook)
.map(|e| e.name.clone())
.collect(),
};
for hook in hooks() {
if on(hook.name)? {
own.hooks.push(hook);
}
}
for section in sections() {
if on(section.name)? {
own.sections.push(section);
}
}
Ok(own)
}
pub fn index_args(
image: &str,
cache_volume: &str,
repo: &std::path::Path,
name: &str,
) -> Vec<String> {
vec![
"run".into(),
"--rm".into(),
"-v".into(),
format!("{}:/work:ro", repo.display()),
"-v".into(),
format!("{cache_volume}:{GRAPH_CACHE}"),
"-w".into(),
"/work".into(),
image.into(),
GRAPH_BIN.into(),
"cli".into(),
"index_repository".into(),
"--repo-path".into(),
"/work".into(),
"--name".into(),
name.into(),
"--mode".into(),
"fast".into(),
]
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
use std::path::Path;
const BUNDLED: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/base");
const FIRST_COMMIT: (u32, u32, u32) = (2026, 8, 5);
fn shipped() -> Manifest {
Manifest::load_dir(Path::new(BUNDLED)).expect("bundled base manifest")
}
#[test]
fn every_base_set_entry_states_its_case() {
let manifest = shipped();
assert!(
!manifest.entries.is_empty(),
"a base set with no entries is not a distribution"
);
for e in &manifest.entries {
assert!(!e.because.trim().is_empty(), "{}: no `because`", e.name);
assert!(
!e.remove.trim().is_empty(),
"{}: no way to remove it",
e.name
);
assert!(
!e.instead_of.is_empty(),
"{}: nothing recorded as considered-instead. An entry with no \
alternatives was not chosen, it was defaulted to.",
e.name
);
assert!(
!e.measured.is_empty(),
"{}: no measured cost. Benefit is argued here, but cost is the \
half that must be measured — it is what creeps.",
e.name
);
assert!(!e.since.trim().is_empty(), "{}: no `since`", e.name);
for m in &e.measured {
for (field, value) in [
("what", &m.what),
("value", &m.value),
("how", &m.how),
("on", &m.on),
] {
assert!(
!value.trim().is_empty(),
"{}: measured `{field}` is blank",
e.name
);
}
parse_ym(&m.on).unwrap_or_else(|| panic!("{}: `{}` is not a date", e.name, m.on));
let day: Vec<u32> = m.on.split('-').filter_map(|p| p.parse().ok()).collect();
assert_eq!(day.len(), 3, "{}: `{}` needs YYYY-MM-DD", e.name, m.on);
assert!(
(day[0], day[1], day[2]) >= FIRST_COMMIT,
"{}: measured {} predates this repository ({}-{:02}-{:02})",
e.name,
m.on,
FIRST_COMMIT.0,
FIRST_COMMIT.1,
FIRST_COMMIT.2
);
}
}
}
#[test]
fn every_base_set_entry_names_its_feature() {
for e in &shipped().entries {
assert!(
!e.feature.trim().is_empty(),
"{}: names no feature. An entry belonging to nothing cannot be \
disabled, because `[omh]` takes feature names.",
e.name
);
}
}
#[test]
fn no_remove_instruction_names_a_path_omh_no_longer_writes() {
for e in &shipped().entries {
assert!(
!e.remove.contains(".omh/profile/"),
"{}: `remove` says `{}`, and that path is not written any more — \
the hooks are generated from this manifest",
e.name,
e.remove
);
}
}
#[test]
fn the_graph_section_explains_the_tools_and_which_project_to_ask() {
let body = section_body("graph-rules");
assert!(body.contains("search_graph"), "must name the tools: {body}");
assert!(
body.to_lowercase().contains("grep"),
"and when not to use them: {body}"
);
assert!(
body.contains("OMH_GRAPH_PROJECT"),
"and which project is its own: {body}"
);
}
#[test]
fn the_git_section_says_the_repair_is_futile_and_what_to_do_instead() {
let body = section_body("git-rules");
assert!(
body.contains("git init"),
"the move it would otherwise make has to be named: {body}"
);
assert!(
body.contains("omh s commit") && body.contains("omh s push"),
"and what the human runs instead: {body}"
);
}
fn section_body(name: &str) -> String {
sections()
.into_iter()
.find(|s| s.name == name)
.unwrap_or_else(|| panic!("{name} is a section omh ships"))
.body
}
#[test]
fn a_feature_gathers_entries_across_kinds() {
let manifest = shipped();
let kinds: BTreeSet<Kind> = manifest
.entries
.iter()
.filter(|e| e.feature == "codegraph")
.map(|e| e.kind)
.collect();
assert_eq!(
kinds,
BTreeSet::from([Kind::Mcp, Kind::Hook, Kind::Rules]),
"codegraph is a server, the hooks that make it used, and the section \
that tells the agent it exists"
);
}
#[test]
fn every_rules_section_costs_what_it_says() {
let manifest = shipped();
for section in sections() {
let entry = manifest
.entry(section.name)
.unwrap_or_else(|| panic!("{} has no manifest entry", section.name));
let claim = &entry.measured[0].value;
let declared: usize = claim
.trim_end_matches(" B")
.replace(',', "")
.trim()
.parse()
.unwrap_or_else(|_| panic!("{}: `{claim}` is not a byte count", section.name));
assert_eq!(
declared,
section.body.len(),
"{}: the manifest claims {declared} B and the section ships {} B. \
Re-measure rather than trimming the prose to fit.",
section.name,
section.body.len()
);
}
}
#[test]
fn removing_a_feature_server_stops_generating_the_rest_of_it() {
let manifest = shipped();
let installed = BTreeSet::from(["memory".to_string()]);
let own = own(&manifest, &BTreeSet::new(), &installed).unwrap();
assert!(
!own.hooks.iter().any(|h| h.name.starts_with("graph-")),
"no graph hook may outlive its server: {:?}",
own.hooks.iter().map(|h| h.name).collect::<Vec<_>>()
);
assert!(
!own.sections.iter().any(|s| s.name == "graph-rules"),
"and neither may the section telling the agent to query it"
);
assert!(
own.sections.iter().any(|s| s.name == "memory-rules"),
"memory is still installed, so its section stays"
);
assert!(
own.hooks.iter().any(|h| h.name == "git-unavailable"),
"git-notice has no server to remove, so nothing about it changes"
);
}
#[test]
fn a_shipped_hook_the_manifest_does_not_describe_is_an_error() {
let dir = manifest_dir(&[("2026.08.toml", &format!("version = \"2026.08\"{ONE_ENTRY}"))]);
let manifest = Manifest::load_dir(dir.path()).unwrap();
let err = own(&manifest, &BTreeSet::new(), &BTreeSet::new())
.expect_err("the binary ships hooks this manifest never mentions");
let err = format!("{err:#}");
assert!(err.contains("graph-refresh"), "must name it: {err}");
assert!(err.contains("omh init"), "and the way out: {err}");
}
#[test]
fn the_grep_nudges_declared_cost_matches_the_string_it_ships() {
let project = project_name("ohmyharness", "s01");
let actual = grep_nudge(&project).len();
let entry = shipped()
.entry("graph-first")
.expect("graph-first in the manifest")
.measured[0]
.value
.clone();
let declared: usize = entry
.trim_end_matches(" B")
.replace(',', "")
.trim()
.parse()
.unwrap_or_else(|_| panic!("graph-first cost `{entry}` is not a byte count"));
assert_eq!(
declared, actual,
"the manifest claims {declared} B; the nudge it ships is {actual} B for project \
`{project}`. Re-measure rather than adjusting the string to fit."
);
}
fn manifest_dir(files: &[(&str, &str)]) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
for (name, body) in files {
std::fs::write(dir.path().join(name), body).unwrap();
}
dir
}
const ONE_ENTRY: &str = r#"
[[entry]]
name = "codegraph"
kind = "mcp"
feature = "codegraph"
since = "2026.06"
because = "b"
remove = "r"
command = "c"
"#;
#[test]
fn a_manifest_an_older_omh_wrote_says_how_to_refresh_it() {
let dir = manifest_dir(&[(
"2026.08.toml",
"version = \"2026.08\"\n[[entry]]\nname = \"codegraph\"\nkind = \"mcp\"\n\
since = \"2026.06\"\nbecause = \"b\"\nremove = \"r\"\ncommand = \"c\"\n",
)]);
let err = format!("{:#}", Manifest::load_dir(dir.path()).unwrap_err());
assert!(err.contains("2026.08.toml"), "must name the file: {err}");
assert!(err.contains("omh init"), "must say the way out: {err}");
}
#[test]
fn a_stray_toml_cannot_become_the_base_set() {
let dir = manifest_dir(&[
("2026.08.toml", &format!("version = \"2026.08\"{ONE_ENTRY}")),
("zz-notes.toml", "version = \"notes\"\n"),
]);
let m = Manifest::load_dir(dir.path()).unwrap();
assert_eq!(m.version, "2026.08");
assert_eq!(m.servers().len(), 1, "the real manifest must win");
}
#[test]
fn versions_are_compared_numerically_not_lexicographically() {
let dir = manifest_dir(&[
("z.toml", &format!("version = \"2027.2\"{ONE_ENTRY}")),
("a.toml", &format!("version = \"2027.10\"{ONE_ENTRY}")),
]);
assert_eq!(Manifest::load_dir(dir.path()).unwrap().version, "2027.10");
}
#[test]
fn a_manifest_naming_nothing_is_an_error_not_an_empty_base_set() {
let dir = manifest_dir(&[("2026.08.toml", "version = \"2026.08\"\n")]);
let err = Manifest::load_dir(dir.path()).unwrap_err().to_string();
assert!(err.contains("no base-set entries"), "got: {err}");
}
#[test]
fn an_empty_directory_says_what_to_do() {
let dir = manifest_dir(&[]);
let err = Manifest::load_dir(dir.path()).unwrap_err().to_string();
assert!(err.contains("omh init"), "got: {err}");
}
#[test]
fn a_loaded_manifest_knows_where_it_came_from() {
let dir = manifest_dir(&[("2026.08.toml", &format!("version = \"2026.08\"{ONE_ENTRY}"))]);
let source = Manifest::load_dir(dir.path()).unwrap().source();
assert!(source.contains("2026.08.toml"), "got: {source}");
assert!(source.contains("2026.08"), "got: {source}");
}
#[test]
fn rejections_say_why_they_were_rejected() {
for r in &shipped().rejected {
assert!(
!r.because.trim().is_empty(),
"{}: rejected with no reason",
r.name
);
}
}
#[test]
fn the_manifest_and_the_code_describe_the_same_base_set() {
let manifest = shipped();
let declared: BTreeSet<&str> = manifest
.entries
.iter()
.filter(|e| e.kind == Kind::Hook)
.map(|e| e.name.as_str())
.collect();
let shipped_hooks: BTreeSet<&str> = hooks().iter().map(|h| h.name).collect();
assert_eq!(
declared, shipped_hooks,
"hooks in the manifest vs hooks in the code"
);
let declared: BTreeSet<&str> = manifest
.entries
.iter()
.filter(|e| e.kind == Kind::Rules)
.map(|e| e.name.as_str())
.collect();
let shipped_sections: BTreeSet<&str> = sections().iter().map(|s| s.name).collect();
assert_eq!(
declared, shipped_sections,
"rules sections in the manifest vs sections in the code"
);
}
#[test]
fn an_mcp_entry_without_a_command_is_not_silently_dropped() {
let manifest = shipped();
let declared = manifest
.entries
.iter()
.filter(|e| e.kind == Kind::Mcp)
.count();
assert_eq!(
declared,
manifest.servers().len(),
"an mcp entry is missing its `command` and would seed nothing"
);
}
#[test]
fn the_document_init_seeds_actually_contains_the_base_set() {
let manifest = shipped();
let body =
serde_json::to_string_pretty(&serde_json::json!({ "mcpServers": manifest.servers() }))
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&body).unwrap();
let servers = parsed["mcpServers"]
.as_object()
.expect("an mcpServers object");
assert!(!servers.is_empty(), "init would seed an empty base set");
assert_eq!(servers["codegraph"]["command"], GRAPH_BIN);
}
#[test]
fn the_base_set_ships_a_code_graph() {
let s = shipped().servers();
assert!(
s.contains_key("codegraph"),
"got: {:?}",
s.keys().collect::<Vec<_>>()
);
assert_eq!(s["codegraph"].command, GRAPH_BIN);
}
#[test]
fn the_memory_server_is_pointed_at_the_directories_omh_mounts() {
let servers = shipped().servers();
let memory = servers
.get(crate::memory::tools::SERVER_KEY)
.expect("the base set must declare the memory server");
assert!(
memory
.args
.iter()
.any(|a| a == crate::memory::GUEST_LOCAL_NOTES),
"the local store is mounted at {}, args say {:?}",
crate::memory::GUEST_LOCAL_NOTES,
memory.args
);
assert!(
memory.args.iter().any(|a| a == "/work/.omh/notes"),
"the team store lives in the checkout: {:?}",
memory.args
);
assert!(
!memory.args.iter().any(|a| a.contains("--session")),
"a session baked into the base set would be wrong for every other one"
);
}
#[test]
fn the_memory_surfaces_declared_cost_matches_what_it_ships() {
let mut server = crate::memory::tools::Server {
team: std::path::PathBuf::from("/nonexistent-team"),
local: std::path::PathBuf::from("/nonexistent-local"),
templates: crate::memory::shipped_templates(),
session: "s01".into(),
client: None,
today: || "2026-08-08".to_string(),
};
let listed = crate::mcp::Tools::list(&mut server);
let actual: usize = listed
.iter()
.map(|t| {
t.name.len()
+ t.description.len()
+ serde_json::to_string(&t.input_schema).unwrap().len()
})
.sum();
let declared = shipped()
.entries
.iter()
.find(|e| e.name == "memory")
.expect("the memory entry")
.measured
.iter()
.find(|m| m.what.contains("injected"))
.expect("an injected-cost measurement")
.value
.trim_end_matches(" B")
.parse::<usize>()
.expect("a byte count");
assert_eq!(
actual, declared,
"re-measure rather than adjusting the surface to fit"
);
}
#[test]
fn base_servers_reference_nothing_on_the_host() {
for (name, server) in shipped().servers() {
assert!(
!server.command.contains('/'),
"{name}: {} is a host path",
server.command
);
for arg in &server.args {
assert!(
!arg.starts_with("/Users") && !arg.starts_with("/home/")
|| arg.starts_with("/home/agent"),
"{name}: {arg} is not a sandbox path"
);
}
}
}
#[test]
fn every_entry_carries_its_argument() {
let manifest = shipped();
let reasons: BTreeMap<_, _> = manifest.rationale().into_iter().collect();
for name in manifest.servers().keys() {
let why = reasons
.get(name.as_str())
.unwrap_or_else(|| panic!("{name} has no rationale"));
assert!(why.len() > 20, "{name}: `{why}` explains nothing");
}
}
#[test]
fn indexing_runs_inside_the_sandbox_with_the_cache_mounted() {
let args = index_args(
"omh/base:x",
"omh-cache-repo",
Path::new("/host/repo"),
"repo",
);
let joined = args.join(" ");
assert!(
joined.contains("omh-cache-repo:"),
"the cache volume must be mounted: {joined}"
);
assert!(
joined.contains(GRAPH_CACHE),
"at the path the server uses: {joined}"
);
assert!(
joined.contains("/host/repo:"),
"the code must be readable: {joined}"
);
}
#[test]
fn indexing_cannot_write_to_the_checkout() {
let joined = index_args("omh/base:x", "vol", Path::new("/host/repo"), "repo").join(" ");
assert!(joined.contains("/host/repo:/work:ro"), "got: {joined}");
}
#[test]
fn every_session_indexes_into_one_named_project() {
let a = index_args("i", "v", Path::new("/host/repo"), "myrepo").join(" ");
let b = index_args("i", "v", Path::new("/host/worktrees/s01"), "myrepo").join(" ");
assert!(a.contains("--name myrepo") && b.contains("--name myrepo"));
}
#[test]
fn indexing_names_the_repository_it_was_given() {
let joined = index_args("i", "v", Path::new("/host/repo"), "r").join(" ");
assert!(joined.contains("--repo-path /work"), "got: {joined}");
}
#[test]
fn indexing_runs_with_the_repo_as_its_working_directory() {
let args = index_args("i", "v", Path::new("/host/repo"), "r");
assert!(
args.windows(2).any(|w| w[0] == "-w" && w[1] == "/work"),
"the project name depends on cwd: {args:?}"
);
}
#[test]
fn a_sessions_graph_is_its_own() {
assert_ne!(project_name("repo", "s01"), project_name("repo", "s02"));
assert_ne!(project_name("alpha", "s01"), project_name("beta", "s01"));
}
#[test]
fn a_sessions_graph_name_is_stable() {
assert_eq!(project_name("repo", "s01"), project_name("repo", "s01"));
}
fn hook(name: &str) -> Hook {
hooks()
.into_iter()
.find(|h| h.name == name)
.unwrap_or_else(|| panic!("no {name} hook"))
}
const ADAPTERS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/adapters");
fn rendered(name: &str) -> crate::hook::Rendered {
let adapter = crate::adapter::Adapter::find(Path::new(ADAPTERS), "claude").unwrap();
let binding = adapter
.supports(crate::adapter::Capability::Hooks)
.expect("claude has hooks");
match crate::hook::render(name, &hook(name).hook, binding, &adapter.tools).unwrap() {
crate::hook::Outcome::Rendered(r) => r,
crate::hook::Outcome::Dropped(d) => panic!("claude cannot express {d}"),
}
}
fn all_rendered() -> Vec<(&'static str, crate::hook::Rendered)> {
hooks()
.into_iter()
.map(|h| (h.name, rendered(h.name)))
.collect()
}
#[test]
fn the_graph_refreshes_when_a_turn_ends() {
let h = rendered("graph-refresh");
assert_eq!(h.event, "Stop");
assert!(h.command.contains("index_repository"), "got: {}", h.command);
assert!(
h.command.contains("/work"),
"it indexes the session, not the checkout"
);
}
#[test]
fn the_agent_is_pointed_at_the_graph_before_it_greps() {
let h = rendered("graph-first");
assert_eq!(h.event, "PreToolUse");
assert!(h.matcher.contains("Grep"), "got: {}", h.matcher);
assert!(
h.command.contains("search_graph"),
"the nudge must name the tool to use: {}",
h.command
);
}
#[test]
fn the_nudge_never_blocks_the_tool() {
let h = rendered("graph-first");
for forbidden in ["exit 1", "deny", "block"] {
assert!(
!h.command.contains(forbidden),
"must not block: {}",
h.command
);
}
}
#[test]
fn hooks_that_query_the_graph_name_their_project_through_the_environment() {
let querying: Vec<_> = all_rendered()
.into_iter()
.filter(|(_, r)| r.command.contains(GRAPH_BIN))
.collect();
assert!(
!querying.is_empty(),
"the filter must still match something"
);
for (name, r) in querying {
assert!(
r.command.contains(PROJECT_ENV),
"{name} must name its project: {}",
r.command
);
}
}
#[test]
fn the_nudge_names_the_project_to_query() {
let h = rendered("graph-first");
assert!(h.command.contains(PROJECT_ENV), "got: {}", h.command);
}
#[test]
fn the_git_notice_fires_on_the_call_that_would_fail() {
let h = rendered("git-unavailable");
assert_eq!(h.event, "PreToolUse");
assert_eq!(h.matcher, "Bash", "git arrives as a shell command");
assert!(
h.command.contains("git init"),
"the repair it would otherwise reach for has to be named: {}",
h.command
);
}
#[test]
fn omhs_hooks_translate_to_opencode_or_are_named() {
let adapter = crate::adapter::Adapter::find(Path::new(ADAPTERS), "opencode").unwrap();
let binding = adapter
.supports(crate::adapter::Capability::Hooks)
.expect("opencode has hooks now");
let own = crate::base::Own {
hooks: hooks(),
..Default::default()
};
let doc = crate::render::document(
crate::adapter::Capability::Hooks,
binding,
&[],
&own,
&Default::default(),
&adapter.tools,
)
.unwrap();
let named: Vec<&str> = doc.dropped.iter().map(|d| d.name.as_str()).collect();
assert_eq!(
named,
vec!["graph-first", "graph-orient", "graph-read"],
"the advisory ones are dropped by name, never downgraded to a wall"
);
for landed in ["graph-refresh", "git-unavailable"] {
assert!(
doc.body.contains(landed),
"a `run` and a `refuse` are what this harness can express: {}",
doc.body
);
}
}
#[test]
fn the_git_notice_blocks_rather_than_advises() {
let git = hooks()
.into_iter()
.find(|h| h.name == "git-unavailable")
.expect("the base set ships it");
assert!(
matches!(git.hook.action, crate::hook::Action::Refuse { .. }),
"got: {:?}",
git.hook.action
);
for name in ["graph-first", "graph-read", "graph-orient"] {
let h = hooks().into_iter().find(|h| h.name == name).unwrap();
assert!(
matches!(h.hook.action, crate::hook::Action::Inject { .. }),
"{name} is a nudge and has to stay one: {:?}",
h.hook.action
);
}
}
#[test]
fn omhs_own_hooks_obey_the_format_they_impose() {
for h in hooks() {
let json = serde_json::to_string(&h.hook).unwrap();
let back = crate::hook::Hook::parse(&json, h.name)
.unwrap_or_else(|e| panic!("{} is not a hook a user could write: {e:#}", h.name));
assert_eq!(back, h.hook, "and it must survive the round trip");
}
}
#[test]
fn every_hook_command_is_valid_shell() {
for (name, r) in all_rendered() {
let out = std::process::Command::new("sh")
.args(["-n", "-c", &r.command])
.output()
.expect("sh must run");
assert!(
out.status.success(),
"{name} is not parseable by sh: {}\n{}",
String::from_utf8_lossy(&out.stderr),
r.command
);
}
}
#[test]
fn every_hook_runs_quietly_when_its_tool_says_nothing() {
let stub = tempfile::tempdir().unwrap();
for name in [GRAPH_BIN, "codebase-memory-mcp"] {
let at = stub.path().join(name);
std::fs::write(&at, "#!/bin/sh\nexit 0\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&at, std::fs::Permissions::from_mode(0o755)).unwrap();
}
}
let path = format!(
"{}:{}",
stub.path().display(),
std::env::var("PATH").unwrap_or_default()
);
for (name, r) in all_rendered() {
let out = std::process::Command::new("sh")
.arg("-c")
.arg(&r.command)
.env("PATH", &path)
.env(PROJECT_ENV, "repo-s01")
.stdin(std::process::Stdio::null())
.output()
.expect("sh must run");
assert!(
out.status.success(),
"{name} exited {:?}: {}",
out.status.code(),
String::from_utf8_lossy(&out.stderr)
);
assert!(
out.stderr.is_empty(),
"{name} wrote to stderr, which the harness shows the user: {}",
String::from_utf8_lossy(&out.stderr)
);
}
}
fn fire_hook(command: &str) -> String {
use std::io::Write;
let mut child = std::process::Command::new("sh")
.arg("-c")
.arg(&rendered("git-unavailable").command)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("sh must run");
let payload = serde_json::json!({ "tool_input": { "command": command } });
child
.stdin
.take()
.unwrap()
.write_all(payload.to_string().as_bytes())
.unwrap();
let out = child.wait_with_output().unwrap();
assert!(
out.stderr.is_empty(),
"the hook must not write to stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap()
}
#[test]
fn the_git_notice_reaches_the_agent_verbatim() {
let fired = fire_hook("git status");
let parsed: serde_json::Value =
serde_json::from_str(&fired).unwrap_or_else(|e| panic!("not JSON: {fired} ({e})"));
let out = &parsed["hookSpecificOutput"];
assert_eq!(
out["hookEventName"].as_str(),
Some("PreToolUse"),
"a decision that does not name its moment is discarded: {fired}"
);
assert_eq!(
out["permissionDecision"].as_str(),
Some("deny"),
"the call is blocked, not merely commented on: {fired}"
);
assert_eq!(
out["permissionDecisionReason"].as_str().unwrap(),
GIT_ABSENT,
"the prose has to survive shell quoting intact"
);
}
#[test]
fn the_git_notice_matches_git_wherever_a_command_can_start() {
for command in [
"git status",
"cd /work && git status",
"cd /work; git init",
"cd /work\ngit status",
" git status",
"echo hi | git apply",
] {
assert!(
!fire_hook(command).trim().is_empty(),
"silent on {command:?}, which is a git call"
);
}
}
#[test]
fn the_git_notice_is_silent_on_everything_else() {
for command in ["cargo test", "ls -la", "echo git", "rg digital"] {
assert!(
fire_hook(command).trim().is_empty(),
"fired on {command:?}, which is not a git call"
);
}
}
#[test]
fn the_ui_build_comes_from_the_release_not_npm() {
let cmd = graph_install();
assert!(cmd.contains("-ui-"), "must fetch the UI variant: {cmd}");
assert!(!cmd.contains("npm install"), "npm cannot deliver it: {cmd}");
}
#[test]
fn the_download_is_checksum_verified() {
let cmd = graph_install();
assert!(cmd.contains("checksums.txt"), "got: {cmd}");
assert!(cmd.contains("sha256sum -c"), "got: {cmd}");
}
#[test]
fn the_download_follows_the_build_architecture() {
let cmd = graph_install();
assert!(
cmd.contains("TARGETARCH") || cmd.contains("dpkg --print-architecture"),
"arch must be derived: {cmd}"
);
}
#[test]
fn serving_the_ui_holds_stdin_open() {
let cmd = ui_command(GRAPH_UI_PORT);
assert!(
cmd.contains("sleep infinity |"),
"stdin must stay open: {cmd}"
);
assert!(cmd.contains("--ui=true"), "got: {cmd}");
}
#[test]
fn the_ui_is_bridged_onto_an_interface_the_host_can_reach() {
let cmd = ui_command(GRAPH_UI_PORT);
assert!(cmd.contains("socat"), "got: {cmd}");
assert!(
cmd.contains(&format!("TCP-LISTEN:{GRAPH_UI_PORT}")),
"must listen where docker publishes: {cmd}"
);
assert!(
cmd.contains(&format!("TCP:127.0.0.1:{GRAPH_UI_INTERNAL}")),
"and forward to where the server binds: {cmd}"
);
}
#[test]
fn removing_a_session_drops_its_graph() {
let cmd = drop_graph_command("ohmyharness-s02").join(" ");
assert!(cmd.contains("delete_project"), "got: {cmd}");
assert!(cmd.contains("ohmyharness-s02"), "got: {cmd}");
}
#[test]
fn dropping_a_graph_that_is_not_there_is_forgiving() {
let cmd = drop_graph_command("nope").join(" ");
assert!(cmd.contains("|| true"), "got: {cmd}");
}
#[test]
fn the_ui_is_named_for_the_repo_not_a_session() {
let c = ui_container("ohmyharness");
assert!(c.contains("ohmyharness"));
assert!(!c.contains("s01"), "not session-scoped: {c}");
assert_eq!(c, ui_container("ohmyharness"), "and stable");
}
#[test]
fn the_ui_container_mounts_only_the_index() {
let args = ui_run_args("omh/base:x", "omh-graph-r", "omh-cache-r", 50000);
let mounts: Vec<&String> = args
.iter()
.skip_while(|a| *a != "-v")
.step_by(2)
.skip(1)
.take(1)
.collect();
assert_eq!(mounts.len(), 1, "exactly one mount: {args:?}");
let joined = args.join(" ");
assert!(joined.contains("omh-cache-r"), "the index: {joined}");
assert!(!joined.contains("/work"), "no worktree: {joined}");
assert!(!joined.contains(".claude"), "no credentials: {joined}");
}
#[test]
fn the_ui_container_publishes_on_loopback_only() {
let joined = ui_run_args("i", "c", "v", 50000).join(" ");
assert!(joined.contains("127.0.0.1:50000:"), "got: {joined}");
assert!(!joined.contains("0.0.0.0"), "got: {joined}");
}
#[test]
fn the_ui_runs_detached_under_its_own_name() {
let args = ui_run_args("i", "omh-graph-r", "v", 1);
assert!(args.contains(&"-d".to_string()), "got: {args:?}");
assert!(args
.windows(2)
.any(|w| w[0] == "--name" && w[1] == "omh-graph-r"));
}
#[test]
fn hooks_speak_through_the_documented_channel() {
for (name, r) in all_rendered() {
if r.event == "Stop" {
continue; }
assert!(
r.command.contains("hookSpecificOutput"),
"{name}: {}",
r.command
);
assert!(
r.command
.contains(&format!(r#""hookEventName":"{}""#, r.event)),
"{name}: a payload that does not name its moment is discarded: {}",
r.command
);
let advises = r.command.contains("additionalContext");
let refuses = r.command.contains("permissionDecision");
assert!(
advises ^ refuses,
"{name} must advise or refuse, and say which: {}",
r.command
);
}
}
#[test]
fn reading_a_file_points_at_the_symbol_lookup() {
let h = rendered("graph-read");
assert_eq!(h.event, "PreToolUse");
assert_eq!(h.matcher, "Read");
assert!(h.command.contains("get_code_snippet"), "got: {}", h.command);
}
#[test]
fn the_read_nudge_stays_silent_when_it_has_nothing_to_say() {
let cmd = rendered("graph-read").command;
assert!(cmd.contains("file_path"), "must inspect the target: {cmd}");
assert!(cmd.contains("wc -c"), "and its size: {cmd}");
}
#[test]
fn a_session_starts_with_the_module_map() {
let h = rendered("graph-orient");
assert_eq!(h.event, "SessionStart");
assert!(h.command.contains("get_architecture"), "got: {}", h.command);
}
#[test]
fn orientation_is_kept_small_because_it_repeats() {
let cmd = rendered("graph-orient").command;
assert!(
!cmd.contains("overview"),
"too broad for something that repeats: {cmd}"
);
for aspect in ["layers", "packages", "boundaries", "entry_points"] {
assert!(cmd.contains(aspect), "missing {aspect}: {cmd}");
}
}
#[test]
fn aspects_are_passed_as_repeated_flags() {
let cmd = rendered("graph-orient").command;
assert!(
!cmd.contains("layers,packages"),
"comma form returns empty: {cmd}"
);
assert_eq!(cmd.matches("--aspects").count(), 4, "got: {cmd}");
}
}