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)

OP OpenVibers committed 15 days ago · 59ae7c9f7b · 2 files, +45 −2

Changes

ddiff --git a/server/http/discovery.js b/server/http/discovery.js
iindex d8959b0..a3bc227 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -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' },
ddiff --git a/test/discovery.test.js b/test/discovery.test.js
iindex 1e66637..fbc3789 100644
---- a/test/discovery.test.js
++++ b/test/discovery.test.js
@@@ -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);