diff --git a/ventures/havre/.well-known/skills/index.json b/ventures/havre/.well-known/skills/index.json
new file mode 100644
index 0000000..476b130
--- /dev/null
+++ b/ventures/havre/.well-known/skills/index.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://schemas.agentskills.io/discovery/0.2.0/schema.json",
+ "skills": [
+ {
+ "name": "havre",
+ "type": "skill-md",
+ "description": "Write AI characters as Havre cards (.havre.json), export them as Character Card V3 for SillyTavern-style apps, talk to them through Venice's API, and remix someone else's character into an original one, with credit.",
+ "url": "/skill.md"
+ }
+ ]
+}
diff --git a/ventures/havre/index.html b/ventures/havre/index.html
index a929226..241dc81 100644
--- a/ventures/havre/index.html
+++ b/ventures/havre/index.html
@@ -19,6 +19,7 @@
+
diff --git a/ventures/havre/robots.txt b/ventures/havre/robots.txt
new file mode 100644
index 0000000..b6b03fc
--- /dev/null
+++ b/ventures/havre/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://havre.yolopreneur.lol/sitemap.txt
diff --git a/ventures/havre/tests/agents.test.mjs b/ventures/havre/tests/agents.test.mjs
new file mode 100644
index 0000000..8e5b612
--- /dev/null
+++ b/ventures/havre/tests/agents.test.mjs
@@ -0,0 +1,37 @@
+// node --test tests/*.test.mjs: where agents look: .well-known/skills/index.json, the alternate links, robots.txt
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { existsSync, readFileSync } from "node:fs";
+
+const root = new URL("../", import.meta.url);
+const read = (p) => readFileSync(new URL(p, root), "utf8");
+
+test("the skills index parses and points at a skill file that exists, with the name its front matter gives", () => {
+ const index = JSON.parse(read(".well-known/skills/index.json"));
+ assert.ok(Array.isArray(index.skills) && index.skills.length >= 1, "at least one skill");
+ for (const s of index.skills) {
+ assert.ok(s.name && s.description, "a name and a description");
+ assert.equal(s.type, "skill-md");
+ assert.match(s.url, /^\/[^/]/, "a path from the site root");
+ assert.ok(existsSync(new URL("." + s.url, root)), `${s.url} is served from the site root`);
+ }
+ const havre = index.skills.find((s) => s.name === "havre");
+ assert.ok(havre, "Havre's skill is listed");
+ assert.equal(havre.url, "/skill.md");
+ assert.match(read("skill.md"), /^---\nname: havre\n/, "skill.md's own front matter agrees on the name");
+});
+
+test("index.html carries the alternate links to llms.txt and skill.md, in the head", () => {
+ const head = read("index.html").split("")[0];
+ assert.match(head, //);
+ assert.match(head, //);
+ for (const href of ["llms.txt", "skill.md"]) assert.ok(existsSync(new URL(href, root)), `${href} exists`);
+});
+
+test("robots.txt allows everything and names the sitemap", () => {
+ const robots = read("robots.txt");
+ assert.match(robots, /^User-agent: \*$/m);
+ assert.match(robots, /^Allow: \/$/m);
+ assert.doesNotMatch(robots, /^Disallow: *\S/m, "nothing is shut out");
+ assert.match(robots, /^Sitemap: https:\/\/havre\.yolopreneur\.lol\/sitemap\.txt$/m);
+});
diff --git a/ventures/havre/tools/build.sh b/ventures/havre/tools/build.sh
index cf6098d..b4b24bc 100644
--- a/ventures/havre/tools/build.sh
+++ b/ventures/havre/tools/build.sh
@@ -13,6 +13,12 @@ cd "$(dirname "$0")/.."
# so the build does
rm -rf art fonts tools/paint.py tools/draw.py tools/vendor tests/smoke.py
+# what agents look for stays: .well-known/ (the skills index) and robots.txt are served as they are, so the build
+# never clears them, and fails if they're gone
+for f in .well-known/skills/index.json robots.txt; do
+ [ -f "$f" ] || { echo "tools/build.sh: $f is missing: agents look for it" >&2; exit 1; }
+done
+
node tools/cast.mjs # cast/index.js, the harbor's own as the module the page imports, from cast/*.havre.json
node --test tests/*.test.mjs