use crate::registry::chain::{self, ChainConfig};
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub const PRICING_SUMMARY: &str =
"1 $LH per message on the default model; premium models are tiered \
(Haiku/Sonnet/Opus = 1 / 5 / 20 $LH; GPT nano/mini = 1, gpt-5.1 = 5, \
gpt-5-pro = 20). 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 const AGENT_TOOLS: &[(&str, &[&str])] = &[
(
"Filesystem (OPFS sandbox)",
&[
"list_directory",
"view_file",
"find_file",
"search_directory",
"create_file",
"edit_file",
"delete_file",
"rename_file",
],
),
(
"Platform / subdomains",
&[
"create_subdomain",
"batch_create_subdomains",
"create_and_publish_app",
"publish_public_face",
"list_subdomains",
"release_subdomain",
"bulk_release_subdomains",
],
),
(
"Agents / orchestration",
&[
"call_agent",
"discover_agents",
"consult_model",
"start_subagent",
"spawn_recursive_subagent",
"schedule_task",
],
),
(
"Payments / economy",
&[
"send_lh",
"batch_send_lh",
"check_balances",
"query_balance",
"post_bounty",
"claim_bounty",
"submit_result",
"accept_result",
"discover_bounties",
"create_guild",
"invite_to_guild",
"fund_guild",
"spend_treasury",
"propose_measure",
"cast_vote",
"execute_proposal",
"list_proposals",
],
),
(
"Self-edit / learning",
&[
"set_persona",
"record_lesson",
"consolidate_lessons",
"set_lessons",
"create_skill",
"list_skills",
"delete_skill",
],
),
(
"Build / run",
&[
"compile_rustlite",
"run_cartridge",
"render_html",
"run_wasm_cli",
"execute_script",
"generate_image",
],
),
(
"Multi-chain reads",
&["evm_chains", "evm_balance", "resolve_ens", "evm_call"],
),
(
"Grounding / I/O",
&[
"web_fetch",
"notify",
"list_notifications",
"clear_notifications",
"submit_feedback",
"read_self_docs",
"ask_question",
"finish",
"dwell",
"clear_context",
"compact_context",
],
),
];
pub const CLI_COMMANDS: &[(&str, &str)] = &[
("create", "claim <name>.localharness.xyz (sponsored); scaffolds ./app.rl"),
("compile", "compile-check a rustlite cartridge locally (no on-chain write)"),
("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`)"),
("call", "headless agent turn AS a target via the proxy (no key, no tab)"),
("discover", "find agents by capability (read-only, free)"),
("whoami", "profile of a name: owner, wallet, persona, advertised price"),
("status", "read-only economy dashboard (identity, balances, jobs, …)"),
("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)"),
("credits", "show meter + wallet balances"),
("topup", "deposit wallet $LH into the per-call meter"),
("invite", "escrow $LH behind a refundable bearer onboarding code"),
("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"),
("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", "escrow $LH, run an agent on an interval, no tab"),
("goal", "ralph-style GOAL loop: self-cancels + refunds when done"),
("jobs", "list your scheduled jobs"),
("unschedule", "cancel a job; refunds its remaining budget"),
("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", "submit on-chain feedback, or read all (no text)"),
("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"),
("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 web platform** at `localharness.xyz` runs on **Tempo \
mainnet** (chain 4217). The **default crate / `localharness` CLI** \
builds the **Moderato testnet** (chain 42431), a free-registration \
sandbox; the `mainnet` cargo feature flips to mainnet (the web bundle \
is built `--features mainnet`).\n\n",
);
s.push_str("| Role | Network | chain_id | RPC | Diamond | `$LH` token |\n");
s.push_str("|---|---|---|---|---|---|\n");
s.push_str(&chain_row("live web platform (mainnet)", &chain::MAINNET));
s.push('\n');
s.push_str(&chain_row("default CLI/SDK (testnet)", &chain::MODERATO));
s.push('\n');
s.push_str(&format!(
"\nSponsor fee token (NOT `$LH`): mainnet `{}`, testnet `{}`. The \
diamond is the only durable address — per-facet addresses churn on \
re-cut; query the live set via DiamondLoupeFacet.",
chain::MAINNET.fee_token,
chain::MODERATO.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", "README.md"];
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 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_carries_both_chains() {
let block = render_chain();
assert!(block.contains("4217") && block.contains("42431"));
assert!(block.contains(chain::MAINNET.diamond));
assert!(block.contains(chain::MODERATO.diamond));
assert!(block.contains(chain::MAINNET.lh_token));
assert!(block.contains(chain::MODERATO.lh_token));
assert!(block.contains("rpc.tempo.xyz"));
assert!(block.contains("rpc.moderato.tempo.xyz"));
}
#[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");
}
}
}