use crate::registry::chain::{self, ChainConfig};
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub const PRICING_SUMMARY: &str =
"Two DISTINCT prices — don't conflate them (feedback #65/#66): \
(1) the platform METER: 1 $LH per message (model round) on the default \
model (Gemini Flash), Claude Opus premium at 20 $LH — pays the PLATFORM \
for LLM inference. (2) the x402 agent-to-agent ASK price: what a target \
agent charges to its OWN wallet per call (default 0.01 $LH unless it \
advertises another via `localharness price`), settled ON TOP of the meter \
only when you pay an agent (`--pay` / `ask_agent`). A 1.00 $LH debit on a \
call is the meter, not a mis-charged ask price. Fiat on-ramp mints on the \
GROSS charged amount at $1 = 100 $LH. $LH is a flat usage credit \
decoupled from the dollar, NOT a stablecoin.";
pub use crate::agent_tools::AGENT_TOOLS;
pub const CLI_COMMANDS: &[(&str, &str)] = &[
("create", "claim <name>.localharness.xyz (sponsored); scaffolds ./app.rl"),
("onboard", "get a brand-new identity its first $LH via an invite (the terminal onboarding entry)"),
("compile", "compile-check a rustlite cartridge locally (no on-chain write); --host-calls dumps its host:: platform surface"),
("receipt", "emit/verify a cartridge's build receipt (hash-committed source → wasm binding)"),
("sh", "run a bashlite script: fs + lh-* commands + `run` composition; value moves (lh-send) need --confirm"),
("publish", "publish a public face (.rl app or .html page; auto-claims if needed)"),
("face", "set the public face: directory | app | html"),
("persona", "publish the agent's on-chain system prompt"),
("price", "advertise a per-call $LH price (or `clear`)"),
("lessons", "show what an agent has learned; --add merges one new lesson on-chain"),
("skills", "list/define/remove an agent's named skills; --export writes agentskills.io SKILL.md folders"),
("state", "export/import the portable agent-state bundle (persona + lessons + skills)"),
("call", "headless agent turn AS a target via the proxy (no key, no tab)"),
("discover", "find agents by capability (read-only, free)"),
("apps", "list published apps in the off-chain app store (read-only, free)"),
("whoami", "profile of a name: owner, wallet, persona, advertised price"),
("status", "read-only economy dashboard (identity, balances, jobs, …)"),
("fee", "what a call to a target costs before paying (read-only, free)"),
("list", "the subdomains you own"),
("models", "list the valid --model ids"),
("redeem", "mint $LH from a one-time bootstrap code"),
("send", "transfer $LH to a 0x address or a name's owner"),
("buy", "buy $LH with a card (fiat on-ramp)"),
("onramp", "fund $LH with USDC.e via the Tempo MPP on-ramp (autonomous, no card)"),
("credits", "show meter + wallet balances; --reclaim pulls unspent meter $LH back to the wallet"),
("topup", "deposit wallet $LH into the per-call meter"),
("invite", "escrow $LH behind a refundable bearer onboarding code"),
("link", "adopt a funded web wallet's seed into a terminal identity (QR seed-adoption)"),
("bounty", "post/list/claim/submit/accept paid work (BountyFacet)"),
("colony", "run one autonomous post→work→judge→pay economy cycle"),
("reputation", "attestation-based on-chain agent trust (alias: rep)"),
("guild", "durable on-chain orgs with a pooled treasury"),
("company", "found a whole company (guild + persona-bearing role subdomains) in one command"),
("party", "ad-hoc squads with an escrowed, pre-agreed split"),
("validation", "ERC-8004 validation staking on a workRef"),
("vote", "guild DAO governance over the treasury"),
("tba", "act through a token-bound account (show/deploy/exec)"),
("room", "encrypted on-chain shared key/value state (SessionRoomFacet)"),
("schedule", "run an agent on an interval, no tab (off-chain, meter-billed)"),
("goal", "ralph GOAL loop: self-ends when the agent declares it done"),
("remind", "tab-free reminder (web-push), OFF-CHAIN + free, no $LH"),
("jobs", "list your scheduled jobs (off-chain + on-chain)"),
("unschedule", "cancel a job (off-chain id or on-chain numeric id)"),
("keeper", "one decentralized-keeper tick: poke all due jobs"),
("notify", "Web Push to your device (or --to <agent>)"),
("threads", "list your saved per-(caller,target) conversations"),
("forget", "drop saved conversation threads"),
("feedback", "file feedback with the maintainers (off-chain telemetry → GitHub issue)"),
("facet", "SolidityLite: deploy/cut your own on-chain facets"),
("mcp", "serve a call_agent tool over stdio MCP"),
("mcp-call", "true x402 MCP-over-HTTP call to a target agent"),
("acp", "serve this agent over the Agent Client Protocol (stdio) for Zed/JetBrains/Buzz"),
("release", "DESTRUCTIVE: burn an owned name (--confirm <name>)"),
];
pub fn open_marker(key: &str) -> String {
format!("<!-- GEN:{key} -->")
}
pub fn close_marker(key: &str) -> String {
format!("<!-- /GEN:{key} -->")
}
pub const KEYS: &[&str] = &["version", "chain", "pricing", "tools", "cli"];
pub fn render(key: &str) -> Option<String> {
match key {
"version" => Some(render_version()),
"chain" => Some(render_chain()),
"pricing" => Some(render_pricing()),
"tools" => Some(render_tools()),
"cli" => Some(render_cli()),
_ => None,
}
}
fn render_version() -> String {
format!(
"**version:** {} (the crate version; the deployed web bundle matches \
crates.io when current)",
version()
)
}
fn chain_row(role: &str, c: &ChainConfig) -> String {
format!(
"| {role} | {} | {} | `{}` | `{}` | `{}` |",
c.name, c.chain_id, c.rpc_url, c.diamond, c.lh_token
)
}
fn render_chain() -> String {
let mut s = String::new();
s.push_str(
"The **live platform** (`localharness.xyz`) and the **`localharness` CLI** \
run on **Tempo mainnet** (chain 4217) — the only chain the platform uses.\n\n",
);
s.push_str("| Role | Network | chain_id | RPC | Diamond | `$LH` token |\n");
s.push_str("|---|---|---|---|---|---|\n");
s.push_str(&chain_row("live platform + CLI", &chain::MAINNET));
s.push('\n');
s.push_str(&format!(
"\nSponsor fee token (NOT `$LH`): `{}`. The diamond is the only durable \
address — per-facet addresses churn on re-cut; query the live set via \
DiamondLoupeFacet.",
chain::MAINNET.fee_token,
));
s
}
fn render_pricing() -> String {
PRICING_SUMMARY.to_string()
}
fn render_tools() -> String {
let mut s = String::new();
for (group, tools) in AGENT_TOOLS {
s.push_str(&format!("- **{group}:** {}\n", tools.join(", ")));
}
s.pop();
s
}
fn render_cli() -> String {
let mut s = String::new();
for (cmd, desc) in CLI_COMMANDS {
s.push_str(&format!("- `localharness {cmd}` — {desc}\n"));
}
s.pop();
s
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct FillReport {
pub changed: Vec<String>,
pub fresh: Vec<String>,
}
impl FillReport {
pub fn drifted(&self) -> bool {
!self.changed.is_empty()
}
}
fn major_minor() -> String {
let v = version();
let mut it = v.split('.');
match (it.next(), it.next()) {
(Some(a), Some(b)) => format!("{a}.{b}"),
_ => v.to_string(),
}
}
fn rewrite_dep_pins(doc: &str) -> String {
let target = major_minor();
const NEEDLE: &str = "localharness = \"";
let mut out = String::with_capacity(doc.len());
let mut rest = doc;
while let Some(i) = rest.find(NEEDLE) {
out.push_str(&rest[..i + NEEDLE.len()]);
let after = &rest[i + NEEDLE.len()..];
match after.find('"') {
Some(end) => {
out.push_str(&target);
rest = &after[end..]; }
None => rest = after,
}
}
out.push_str(rest);
out
}
pub fn fill(doc: &str) -> (String, FillReport) {
let mut out = String::with_capacity(doc.len() + 256);
let mut report = FillReport::default();
let mut rest = doc;
const OPEN_PREFIX: &str = "<!-- GEN:";
loop {
let Some(open_abs) = rest.find(OPEN_PREFIX) else {
out.push_str(rest);
break;
};
let after_prefix = &rest[open_abs + OPEN_PREFIX.len()..];
let Some(key_end) = after_prefix.find(" -->") else {
let consumed = open_abs + OPEN_PREFIX.len();
out.push_str(&rest[..consumed]);
rest = &rest[consumed..];
continue;
};
let key = after_prefix[..key_end].trim().to_string();
let close_marker = close_marker(&key);
let Some(close_rel) = rest[open_abs..].find(&close_marker) else {
let consumed = open_abs + OPEN_PREFIX.len();
out.push_str(&rest[..consumed]);
rest = &rest[consumed..];
continue;
};
let close_abs = open_abs + close_rel;
let block_end = close_abs + close_marker.len();
out.push_str(&rest[..open_abs]);
match render(&key) {
Some(fresh) => {
let new_block = format!("{}\n{fresh}\n{close_marker}", open_marker(&key));
let old_block = &rest[open_abs..block_end];
if old_block == new_block {
report.fresh.push(key);
} else {
report.changed.push(key);
}
out.push_str(&new_block);
}
None => {
out.push_str(&rest[open_abs..block_end]);
}
}
rest = &rest[block_end..];
}
let pinned = rewrite_dep_pins(&out);
if pinned != out {
report.changed.push("dep-version".to_string());
}
(pinned, report)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
const MANAGED_DOCS: &[&str] = &["web/skill.md", "web/llms.txt"];
fn read_doc(rel: &str) -> String {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(rel);
std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("{} must exist and be readable: {e}", path.display()))
}
#[test]
fn no_doc_drift() {
let mut stale = Vec::new();
for rel in MANAGED_DOCS {
let doc = read_doc(rel);
let (_filled, report) = fill(&doc);
for key in &report.changed {
stale.push(format!(" {rel}: GEN:{key}"));
}
}
assert!(
stale.is_empty(),
"doc drift: the following GEN blocks are stale —\n{}\n\nrun `cargo run --bin gen-docs` to regenerate.",
stale.join("\n")
);
}
#[test]
fn claude_md_facts_not_stale() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let Ok(claude) = std::fs::read_to_string(root.join("CLAUDE.md")) else {
eprintln!("skip: CLAUDE.md not present (packaged crate)");
return;
};
let is_num = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit());
for (i, _) in claude.match_indices("(**") {
let rest = &claude[i + 3..];
let Some(end) = rest.find("**)") else { continue };
let mut parts = rest[..end].split('.');
if let (Some(a), Some(b), Some(c), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
{
assert!(
!(is_num(a) && is_num(b) && (c == "x" || is_num(c))),
"CLAUDE.md pins a crate-version literal `(**{a}.{b}.{c}**)` — it WILL go \
stale; point at Cargo.toml/crates.io instead."
);
}
}
let canon: String = ["src/registry/chain.rs", "src/app/sponsor.rs"]
.iter()
.map(|p| std::fs::read_to_string(root.join(p)).unwrap_or_default())
.collect();
let bytes = claude.as_bytes();
let mut i = 0;
while i + 42 <= bytes.len() {
if bytes[i] == b'0' && bytes[i + 1] == b'x' {
let hexlen = claude[i + 2..].bytes().take_while(u8::is_ascii_hexdigit).count();
if hexlen == 40 {
let addr = &claude[i..i + 42];
assert!(
canon.contains(addr),
"CLAUDE.md carries address `{addr}` that is not verbatim in \
src/registry/chain.rs or src/app/sponsor.rs — don't hand-copy \
addresses into the project map; point at chain.rs."
);
}
i += 2 + hexlen.max(1);
} else {
i += 1;
}
}
}
#[test]
fn no_stale_dep_pin() {
let mm = major_minor();
for rel in MANAGED_DOCS {
let doc = read_doc(rel);
for line in doc.lines().filter(|l| l.contains("localharness = \"")) {
assert!(
line.contains(&format!("localharness = \"{mm}\"")),
"{rel}: stale dependency pin (`{}`) — should be `localharness = \"{mm}\"`; run gen-docs.",
line.trim()
);
}
}
}
#[test]
fn system_prompt_has_no_hardcoded_version() {
let v = version();
for rel in ["src/app/chat/prompt.rs", "src/app/self_docs.rs"] {
let src = read_doc(rel);
assert!(
!src.contains(v),
"{rel} hardcodes the crate version {v:?} — derive it (env!/self_docs) so the agent's prompt can't go stale.",
);
}
}
#[test]
fn every_managed_doc_has_gen_blocks() {
for rel in MANAGED_DOCS {
let doc = read_doc(rel);
assert!(
doc.contains("<!-- GEN:"),
"{rel} has no GEN blocks — the doc-integrity system can't manage its facts"
);
}
}
#[test]
fn fill_is_idempotent() {
let sample = "intro\n<!-- GEN:version -->\nSTALE\n<!-- /GEN:version -->\noutro\n";
let (once, r1) = fill(sample);
assert!(r1.drifted(), "the STALE block should have been rewritten");
let (twice, r2) = fill(&once);
assert_eq!(once, twice, "second fill must be a no-op");
assert!(!r2.drifted(), "second fill must report no drift");
}
#[test]
fn unknown_key_untouched() {
let sample = "<!-- GEN:bogus -->\nkeep me\n<!-- /GEN:bogus -->";
let (out, report) = fill(sample);
assert_eq!(out, sample);
assert!(report.changed.is_empty() && report.fresh.is_empty());
}
#[test]
fn chain_block_is_mainnet_only_with_real_values() {
let block = render_chain();
assert!(block.contains("4217"), "must state the mainnet chain id");
assert!(block.contains(chain::MAINNET.diamond));
assert!(block.contains(chain::MAINNET.lh_token));
assert!(block.contains("rpc.tempo.xyz"));
let lower = block.to_lowercase();
assert!(!lower.contains("moderato"), "README must not mention the testnet");
assert!(!lower.contains("testnet"), "README must not mention the testnet");
assert!(!block.contains("42431"), "README must not mention the testnet chain id");
}
#[test]
fn version_block_matches_cargo() {
assert!(render_version().contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn all_keys_render() {
for k in KEYS {
assert!(render(k).is_some(), "KEY {k} has no renderer");
}
}
}