use std::path::Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Stack {
pub name: &'static str,
pub marker: &'static str,
pub test: &'static str,
pub format: &'static str,
}
const KNOWN: [Stack; 4] = [
Stack {
name: "rust",
marker: "Cargo.toml",
test: "cargo test",
format: "cargo fmt",
},
Stack {
name: "node",
marker: "package.json",
test: "npm test",
format: "npm run format",
},
Stack {
name: "python",
marker: "pyproject.toml",
test: "pytest",
format: "ruff format .",
},
Stack {
name: "go",
marker: "go.mod",
test: "go test ./...",
format: "gofmt -w .",
},
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Seed {
pub source: String,
pub fact: String,
}
pub fn stacks(repo: &Path) -> Vec<Stack> {
KNOWN
.into_iter()
.filter(|s| repo.join(s.marker).exists())
.collect()
}
pub fn preferred_harness(
candidates: &[String],
installed_on_host: &dyn Fn(&str) -> bool,
) -> Option<String> {
candidates
.iter()
.find(|c| installed_on_host(c))
.or_else(|| candidates.first())
.cloned()
}
pub fn agents_md(stacks: &[Stack]) -> String {
let mut out = String::from(
"# Project rules\n\n<!-- generated by `omh init`; edit freely, omh will not overwrite -->\n\n\
## 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\n\
## 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",
);
out.push_str(&git_rules());
out.push_str(&memory_rules());
if stacks.is_empty() {
out.push_str(
"\nNo stack detected. Add your build, test, and lint commands here so\n\
every harness runs the same ones.\n",
);
return out;
}
for s in stacks {
out.push_str(&format!(
"\n## {}\n\nDetected from `{}`.\n\n- test: `{}`\n- format: `{}`\n",
s.name, s.marker, s.test, s.format
));
}
out
}
pub fn memory_rules() -> String {
format!(
"\n## 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,
)
}
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 git_rules() -> String {
format!("\n## Git\n\n{GIT_ABSENT}\n")
}
pub fn seeds(repo: &Path) -> Vec<Seed> {
let mut out = Vec::new();
if let Ok(readme) = std::fs::read_to_string(repo.join("README.md")) {
if let Some(line) = readme
.lines()
.map(|l| l.trim().trim_start_matches('>').trim())
.find(|l| {
!l.is_empty()
&& !l.starts_with('#')
&& !l.starts_with("[!")
&& !l.starts_with("![")
&& !l.starts_with("---")
})
{
out.push(Seed {
source: "README.md".into(),
fact: line.to_string(),
});
}
}
for s in stacks(repo) {
out.push(Seed {
source: s.marker.into(),
fact: format!(
"stack: {} (test `{}`, format `{}`)",
s.name, s.test, s.format
),
});
}
for rules in ["AGENTS.md", "CLAUDE.md"] {
if let Ok(body) = std::fs::read_to_string(repo.join(rules)) {
if !body.trim().is_empty() {
out.push(Seed {
source: rules.into(),
fact: format!("existing conventions ({} lines)", body.lines().count()),
});
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn repo(files: &[(&str, &str)]) -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("repo");
std::fs::create_dir_all(&root).unwrap();
for (name, body) in files {
let p = root.join(name);
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
std::fs::write(p, body).unwrap();
}
(dir, root)
}
#[test]
fn a_manifest_identifies_the_stack() {
let (_d, r) = repo(&[("Cargo.toml", "[package]")]);
let found = stacks(&r);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "rust");
}
#[test]
fn a_polyglot_repo_reports_every_stack() {
let (_d, r) = repo(&[("Cargo.toml", ""), ("package.json", "{}")]);
let names: Vec<_> = stacks(&r).into_iter().map(|s| s.name).collect();
assert_eq!(names, ["rust", "node"]);
}
#[test]
fn no_marker_means_no_stack_rather_than_a_guess() {
let (_d, r) = repo(&[("README.md", "hello")]);
assert!(stacks(&r).is_empty());
}
#[test]
fn every_known_stack_supplies_commands() {
for s in KNOWN {
assert!(!s.test.is_empty(), "{} has no test command", s.name);
assert!(!s.format.is_empty(), "{} has no format command", s.name);
}
}
#[test]
fn generated_rules_name_the_detected_commands() {
let (_d, r) = repo(&[("Cargo.toml", "")]);
let body = agents_md(&stacks(&r));
assert!(body.contains("cargo test"), "got: {body}");
assert!(body.contains("cargo fmt"), "got: {body}");
}
#[test]
fn generated_rules_explain_the_code_graph() {
let body = agents_md(&[]);
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 — the store holds every session's: {body}"
);
}
#[test]
fn generated_rules_say_git_is_absent_by_design_and_what_to_do_instead() {
let body = agents_md(&[]);
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}"
);
}
#[test]
fn generated_rules_stay_useful_with_no_stack_detected() {
let body = agents_md(&[]);
assert!(!body.trim().is_empty(), "must still produce a usable file");
}
fn candidates() -> Vec<String> {
vec!["claude".into(), "opencode".into()]
}
#[test]
fn host_evidence_picks_the_default_harness() {
let pick = preferred_harness(&candidates(), &|h| h == "opencode");
assert_eq!(pick.as_deref(), Some("opencode"));
}
#[test]
fn nothing_installed_still_yields_a_default() {
let pick = preferred_harness(&candidates(), &|_| false);
assert_eq!(pick.as_deref(), Some("claude"));
}
#[test]
fn no_adapters_means_no_preference() {
assert_eq!(preferred_harness(&[], &|_| true), None);
}
#[test]
fn seeds_are_derived_from_what_the_repo_already_says() {
let (_d, r) = repo(&[
("README.md", "# omh\n\noh-my-zsh for agentic coding.\n"),
("Cargo.toml", "[package]\nname = \"omh\""),
]);
let s = seeds(&r);
assert!(
s.iter().any(|x| x.fact.contains("oh-my-zsh")),
"README should seed the project description: {s:?}"
);
assert!(
s.iter().any(|x| x.fact.contains("rust")),
"stack should be seeded: {s:?}"
);
}
#[test]
fn a_blockquote_tagline_is_read_as_prose() {
let (_d, r) = repo(&[("README.md", "# omh\n\n> Launch any coding harness.\n")]);
let s = seeds(&r);
let fact = &s
.iter()
.find(|x| x.source == "README.md")
.expect("README seed")
.fact;
assert_eq!(
fact, "Launch any coding harness.",
"markdown syntax is not the fact"
);
}
#[test]
fn a_badge_line_is_not_mistaken_for_a_description() {
let (_d, r) = repo(&[(
"README.md",
"# p\n\n[](https://ci)\n\nA real description.\n",
)]);
let s = seeds(&r);
let fact = &s
.iter()
.find(|x| x.source == "README.md")
.expect("README seed")
.fact;
assert_eq!(fact, "A real description.");
}
#[test]
fn every_seed_cites_its_source() {
let (_d, r) = repo(&[("README.md", "# p\n\nA thing.\n"), ("go.mod", "module x")]);
for seed in seeds(&r) {
assert!(!seed.source.is_empty(), "unsourced seed: {seed:?}");
}
}
#[test]
fn an_empty_repo_seeds_nothing_rather_than_inventing() {
let (_d, r) = repo(&[]);
assert!(seeds(&r).is_empty());
}
#[test]
fn existing_rules_are_seeded_so_conventions_survive() {
let (_d, r) = repo(&[("AGENTS.md", "# Rules\n\nTDD always.\n")]);
let s = seeds(&r);
assert!(
s.iter().any(|x| x.source.contains("AGENTS.md")),
"got: {s:?}"
);
}
}