openvibers/openvibe.codes Commits
llms.txt: the harness router is described, not linked (it answers POST only): POST /api/v1/harnesses/route was listed as a fetchable URL and answers 404 to a GET, so a language model following it got nothing. The endpoint stays described as a plain line in the section body, and the agent manifest takes its place as the GET link. The llms.txt test now checks that every URL the file lists answers and that the POST-only endpoint is described, not linked. (#150)
Changes
@@@ -84,6 +84,27 @@ function describePage(pathname, { governance = [] }) {return { title: pathname, text: '' };}++/**++ * llms.txt: the shared kit's llmsTxt, with one thing it cannot say. A POST-only endpoint is not a++ * page to fetch — a link to it is a URL that answers 404 to a GET, and a language model that++ * follows it gets nothing. So a section may carry notes: plain lines that describe such an++ * endpoint (POST /api/v1/route — ...) instead of linking it. Every real GET link stays a link.++ */++function llmsTxt({ name, summary, details, sections = [] }) {++ const out = [`# ${name}`, '', `> ${String(summary || '').replace(/\s+/g, ' ').trim()}`, ''];++ if (details) out.push(String(details).trim(), '');++ for (const sec of sections) {++ if (!sec) continue;++ const links = (sec.links || []).map((l) => '- [' + l.title + '](' + l.url + ')' + (l.note ? ': ' + String(l.note).replace(/\s+/g, ' ').trim() : ''));++ const notes = (sec.notes || []).map((n) => String(n).replace(/\s+/g, ' ').trim());++ if (!links.length && !notes.length) continue;++ out.push('## ' + sec.title, '');++ if (links.length) out.push(...links, '');++ if (notes.length) out.push(...notes, '');++ }++ return out.join('\n');++}++function createDiscoveryRoutes(ctx) {const { config, governance = [] } = ctx;const r = asyncRouter();@@@ -99,7 +120,7 @@ function createDiscoveryRoutes(ctx) {});r.get('/llms.txt', (_req, res) => {-- res.type('text/plain').set('Cache-Control', TEXT).send(seo.llmsTxt({++ res.type('text/plain').set('Cache-Control', TEXT).send(llmsTxt({name: SITE_NAME,summary: 'OpenVibe.Codes: the open coding-agent harness. openvibe-codes runs a coding task on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own OpenAI-compatible model, hands it to the next agent when one fails, and keeps every run; plus a public catalog and routing API, and the way to improve OpenVibe itself.',details: `Every page is server-rendered and readable without JavaScript. The harness is a command-line tool and a Node.js module that runs on your machine with your agents and keys (install: ${INSTALL_HINT}). Every agent's output becomes one event stream in Claude Code's stream-json shape. The catalog and the router are public: GET /api/v1/harnesses lists every harness with its models, and POST /api/v1/harnesses/route picks one for a task by capability first, then cost. A hosted runner comes next. The developer console that used to live here (projects, apps, keys, the API reference, OAuth and webhook tools, releases) is OpenVibe.Services at ${SERVICES_ORIGIN}.`,@@@ -117,10 +138,12 @@ function createDiscoveryRoutes(ctx) {] },{ title: 'Machine-readable', links: [{ title: 'The agent catalog (JSON)', url: abs('/api/v1/harnesses'), note: 'every harness with its agents (platform.harness-offer@1 rows)' },-- { title: 'Route a task', url: abs('/api/v1/harnesses/route'), note: 'POST { task, requirements? } → { selected, reasons, candidates }' },++ { title: 'The agent manifest (JSON)', url: abs('/.well-known/agent.json'), note: 'what this site tells an agent about itself' },{ title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages, each with a real lastmod' },{ title: 'Full text for language models', url: abs('/llms-full.txt'), note: 'every public page, one line each' },{ title: 'Release metadata (JSON)', url: abs('/release.json'), note: 'this service\'s current release, per ADR-016' },++ ], notes: [++ 'POST /api/v1/harnesses/route — pick a harness for a task: send { task, requirements? } and get { selected, reasons, candidates }. It answers POST only, so there is no page here to fetch; the catalog and the router are described at /harnesses and /start.',] },{ title: 'Elsewhere', links: [{ title: 'OpenVibe.Services', url: SERVICES_ORIGIN, note: 'the developer platform: projects, keys, grants and the API docs of every service' },@@@ -55,6 +55,26 @@ const PRIVATE = ['/projects', '/staff', '/auth/', '/oauth/test-callback'];for (const p of PRIVATE) assert.ok(!r.text.includes(`https://openvibe.codes${p}`), `private path in llms.txt: ${p}`);});++ await check('llms.txt: every URL it lists answers a GET, and the POST-only endpoint is described, not linked', async () => {++ const r = await t.get('/llms.txt');++ const SITE = 'https://openvibe.codes';++ // Every link the file names must answer when it is fetched as a link.++ const links = r.text.split('](').slice(1).map((s) => s.split(')')[0]).filter((u) => u.startsWith('http'));++ assert.ok(links.length >= 8, 'too few links: ' + links.length);++ for (const url of links) {++ if (!url.startsWith(SITE)) continue; // the other OpenVibe sites are not this app to fetch++ const res = await t.get(new URL(url).pathname);++ assert.ok(res.status < 400, url + ' answers ' + res.status);++ }++ // The router answers POST only, so it is described in the section body, never linked.++ const endpoint = '/api/v1/harnesses/route';++ assert.ok(!r.text.includes('](' + SITE + endpoint + ')'), 'the POST-only endpoint is still a link');++ const described = r.text.split('\n').find((l) => l.startsWith('POST ' + endpoint + ' — '));++ assert.ok(described, 'the POST-only endpoint is not described');++ assert.ok(described.includes('POST only'), 'the description does not say it answers POST only');++ assert.ok(!described.includes(']('), 'the described POST endpoint is a link');++ assert.ok(!described.includes('http'), 'the described POST endpoint names a URL to fetch');++ });await check('llms-full.txt: 200 text/plain, the site title, and every page the sitemap lists', async () => {const r = await t.get('/llms-full.txt');assert.strictEqual(r.status, 200);
@@ -84,6 +84,27 @@ function describePage(pathname, { governance = [] }) {
@@ -84,6 +84,27 @@ function describePage(pathname, { governance = [] }) {
return { title: pathname, text: '' };
return { title: pathname, text: '' };
}
}
+/**
+ * llms.txt: the shared kit's llmsTxt, with one thing it cannot say. A POST-only endpoint is not a
+ * page to fetch — a link to it is a URL that answers 404 to a GET, and a language model that
+ * follows it gets nothing. So a section may carry notes: plain lines that describe such an
+ * endpoint (POST /api/v1/route — ...) instead of linking it. Every real GET link stays a link.
+ */
+function llmsTxt({ name, summary, details, sections = [] }) {
+ const out = [`# ${name}`, '', `> ${String(summary || '').replace(/\s+/g, ' ').trim()}`, ''];
+ if (details) out.push(String(details).trim(), '');
+ for (const sec of sections) {
+ if (!sec) continue;
+ const links = (sec.links || []).map((l) => '- [' + l.title + '](' + l.url + ')' + (l.note ? ': ' + String(l.note).replace(/\s+/g, ' ').trim() : ''));
+ const notes = (sec.notes || []).map((n) => String(n).replace(/\s+/g, ' ').trim());
+ if (!links.length && !notes.length) continue;
+ out.push('## ' + sec.title, '');
+ if (links.length) out.push(...links, '');
+ if (notes.length) out.push(...notes, '');
+ }
+ return out.join('\n');
+}
+
function createDiscoveryRoutes(ctx) {
function createDiscoveryRoutes(ctx) {
const { config, governance = [] } = ctx;
const { config, governance = [] } = ctx;
const r = asyncRouter();
const r = asyncRouter();
@@ -99,7 +120,7 @@ function createDiscoveryRoutes(ctx) {
@@ -99,7 +120,7 @@ function createDiscoveryRoutes(ctx) {
});
});
r.get('/llms.txt', (_req, res) => {
r.get('/llms.txt', (_req, res) => {
- res.type('text/plain').set('Cache-Control', TEXT).send(seo.llmsTxt({
+ res.type('text/plain').set('Cache-Control', TEXT).send(llmsTxt({
name: SITE_NAME,
name: SITE_NAME,
summary: 'OpenVibe.Codes: the open coding-agent harness. openvibe-codes runs a coding task on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own OpenAI-compatible model, hands it to the next agent when one fails, and keeps every run; plus a public catalog and routing API, and the way to improve OpenVibe itself.',
summary: 'OpenVibe.Codes: the open coding-agent harness. openvibe-codes runs a coding task on Claude Code, Codex, OpenCode, Command Code, Aider, the DeepSeek API or your own OpenAI-compatible model, hands it to the next agent when one fails, and keeps every run; plus a public catalog and routing API, and the way to improve OpenVibe itself.',
details: `Every page is server-rendered and readable without JavaScript. The harness is a command-line tool and a Node.js module that runs on your machine with your agents and keys (install: ${INSTALL_HINT}). Every agent's output becomes one event stream in Claude Code's stream-json shape. The catalog and the router are public: GET /api/v1/harnesses lists every harness with its models, and POST /api/v1/harnesses/route picks one for a task by capability first, then cost. A hosted runner comes next. The developer console that used to live here (projects, apps, keys, the API reference, OAuth and webhook tools, releases) is OpenVibe.Services at ${SERVICES_ORIGIN}.`,
details: `Every page is server-rendered and readable without JavaScript. The harness is a command-line tool and a Node.js module that runs on your machine with your agents and keys (install: ${INSTALL_HINT}). Every agent's output becomes one event stream in Claude Code's stream-json shape. The catalog and the router are public: GET /api/v1/harnesses lists every harness with its models, and POST /api/v1/harnesses/route picks one for a task by capability first, then cost. A hosted runner comes next. The developer console that used to live here (projects, apps, keys, the API reference, OAuth and webhook tools, releases) is OpenVibe.Services at ${SERVICES_ORIGIN}.`,
@@ -117,10 +138,12 @@ function createDiscoveryRoutes(ctx) {
@@ -117,10 +138,12 @@ function createDiscoveryRoutes(ctx) {
] },
] },
{ title: 'Machine-readable', links: [
{ title: 'Machine-readable', links: [
{ title: 'The agent catalog (JSON)', url: abs('/api/v1/harnesses'), note: 'every harness with its agents (platform.harness-offer@1 rows)' },
{ title: 'The agent catalog (JSON)', url: abs('/api/v1/harnesses'), note: 'every harness with its agents (platform.harness-offer@1 rows)' },
- { title: 'Route a task', url: abs('/api/v1/harnesses/route'), note: 'POST { task, requirements? } → { selected, reasons, candidates }' },
+ { title: 'The agent manifest (JSON)', url: abs('/.well-known/agent.json'), note: 'what this site tells an agent about itself' },
{ title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages, each with a real lastmod' },
{ title: 'Sitemap', url: abs('/sitemap.xml'), note: 'the public pages, each with a real lastmod' },
{ title: 'Full text for language models', url: abs('/llms-full.txt'), note: 'every public page, one line each' },
{ title: 'Full text for language models', url: abs('/llms-full.txt'), note: 'every public page, one line each' },
{ title: 'Release metadata (JSON)', url: abs('/release.json'), note: 'this service\'s current release, per ADR-016' },
{ title: 'Release metadata (JSON)', url: abs('/release.json'), note: 'this service\'s current release, per ADR-016' },
+ ], notes: [
+ 'POST /api/v1/harnesses/route — pick a harness for a task: send { task, requirements? } and get { selected, reasons, candidates }. It answers POST only, so there is no page here to fetch; the catalog and the router are described at /harnesses and /start.',
] },
] },
{ title: 'Elsewhere', links: [
{ title: 'Elsewhere', links: [
{ title: 'OpenVibe.Services', url: SERVICES_ORIGIN, note: 'the developer platform: projects, keys, grants and the API docs of every service' },
{ title: 'OpenVibe.Services', url: SERVICES_ORIGIN, note: 'the developer platform: projects, keys, grants and the API docs of every service' },
@@ -55,6 +55,26 @@ const PRIVATE = ['/projects', '/staff', '/auth/', '/oauth/test-callback'];
@@ -55,6 +55,26 @@ const PRIVATE = ['/projects', '/staff', '/auth/', '/oauth/test-callback'];
for (const p of PRIVATE) assert.ok(!r.text.includes(`https://openvibe.codes${p}`), `private path in llms.txt: ${p}`);
for (const p of PRIVATE) assert.ok(!r.text.includes(`https://openvibe.codes${p}`), `private path in llms.txt: ${p}`);
});
});
+ await check('llms.txt: every URL it lists answers a GET, and the POST-only endpoint is described, not linked', async () => {
+ const r = await t.get('/llms.txt');
+ const SITE = 'https://openvibe.codes';
+ // Every link the file names must answer when it is fetched as a link.
+ const links = r.text.split('](').slice(1).map((s) => s.split(')')[0]).filter((u) => u.startsWith('http'));
+ assert.ok(links.length >= 8, 'too few links: ' + links.length);
+ for (const url of links) {
+ if (!url.startsWith(SITE)) continue; // the other OpenVibe sites are not this app to fetch
+ const res = await t.get(new URL(url).pathname);
+ assert.ok(res.status < 400, url + ' answers ' + res.status);
+ }
+ // The router answers POST only, so it is described in the section body, never linked.
+ const endpoint = '/api/v1/harnesses/route';
+ assert.ok(!r.text.includes('](' + SITE + endpoint + ')'), 'the POST-only endpoint is still a link');
+ const described = r.text.split('\n').find((l) => l.startsWith('POST ' + endpoint + ' — '));
+ assert.ok(described, 'the POST-only endpoint is not described');
+ assert.ok(described.includes('POST only'), 'the description does not say it answers POST only');
+ assert.ok(!described.includes(']('), 'the described POST endpoint is a link');
+ assert.ok(!described.includes('http'), 'the described POST endpoint names a URL to fetch');
+ });
await check('llms-full.txt: 200 text/plain, the site title, and every page the sitemap lists', async () => {
await check('llms-full.txt: 200 text/plain, the site title, and every page the sitemap lists', async () => {
const r = await t.get('/llms-full.txt');
const r = await t.get('/llms-full.txt');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);