openvibers/openvibe.actor Commits
openvibe-shared v4.108.0 (was v4.107.4) (#283)
Changes
@@@ -258,5 +258,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [<!-- versions:start -->- openvibe-contracts: v0.269.0- openvibe-sdk: v0.82.0--- openvibe-shared: v4.107.4++- openvibe-shared: v4.108.0<!-- versions:end -->@@@ -9,11 +9,11 @@"domain": "openvibe.actor","contracts": "openvibe-contracts v0.269.0","sdk": "openvibe-sdk v0.82.0",-- "shared": "openvibe-shared v4.107.4",++ "shared": "openvibe-shared v4.108.0","packages": ["openvibe-contracts v0.269.0","openvibe-sdk v0.82.0",-- "openvibe-shared v4.107.4"++ "openvibe-shared v4.108.0"],"summary": "Public at https://openvibe.actor (sudo ovhost deploy actor; unit openvibe-actor on 127.0.0.1:4950). A task in plain words is classified, routed by openvibe-sdk/placement to the agent system that can do it (the OpenVibe runtime acting through OpenVibe.Search and OpenVibe.Tools, OpenAI's web agent, an open model on OpenVibe's server for private mode), run with its steps streamed live, checked by a model of another family, and delivered as platform.task@1 with its cost to a fraction of a cent and the explanation; a failed agent or check hands it to the next agent. Free tier per person with an operator ceiling.","works": [@@@ -17,7 +17,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.269.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0","pg": "^8.23.0"},"devDependencies": {@@@ -1190,9 +1190,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.107.4",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",-- "integrity": "sha512-tThN0m3KMncEHVcBOvE+ysHjsZTlVIDLG1P+/i2Mulmxfc/AUpebAXhpWWrut50OChZ8o+HeePxsSrPI8mUQOQ==",++ "version": "4.108.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",++ "integrity": "sha512-NPzwjxpzGIpFpIm4uw3+VwHfsxbJ2+cD9yuhRzSwolSwqv2kCmwaVtHJ0DA9gKO3K8PCBfIP3UpKzpuSbYEJ+g==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0",@@@ -30,7 +30,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.269.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0","pg": "^8.23.0"},"engines": {@@@ -8,6 +8,7 @@const fs = require('fs');const path = require('path');const seo = require('openvibe-shared/seo');++const { twinUrl } = require('openvibe-shared/agent-layer');const cache = require('openvibe-shared/cache-policy');const { asyncRouter } = require('./router');@@@ -52,12 +53,22 @@ const publicPages = () => [* 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 = [] }) {++// AGT-5: each of the site's own pages is linked at its Markdown twin (/agents → /agents.md, / → /index.md); a file, an++// /api/ or /.well-known/ path and another site's link keep their URL, as openvibe-shared's seo.llmsTxt markdown mode++// does. `optional` is llmstxt.org's "## Optional", last.++function twinOf(url, base) {++ let u;++ try { u = new URL(url); } catch { return url; }++ if (!base || u.origin !== new URL(base).origin || /^\/(api|\.well-known)(\/|$)/.test(u.pathname) || /\.[a-z0-9]{1,8}$/i.test(u.pathname.split('/').pop())) return url;++ return twinUrl(url);++}++++function llmsTxt({ name, summary, details, sections = [], optional = [], base = null }) {const out = [`# ${name}`, '', `> ${String(summary || '').replace(/\s+/g, ' ').trim()}`, ''];if (details) out.push(String(details).trim(), '');-- for (const sec of sections) {++ for (const sec of [...sections, ...(optional.length ? [{ title: 'Optional', links: optional }] : [])]) {if (!sec) continue;-- const links = (sec.links || []).map((l) => '- [' + l.title + '](' + l.url + ')' + (l.note ? ': ' + String(l.note).replace(/\s+/g, ' ').trim() : ''));++ const links = (sec.links || []).map((l) => '- [' + l.title + '](' + twinOf(l.url, base) + ')' + (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, '');@@@ -84,6 +95,7 @@ function createDiscoveryRoutes(ctx) {r.get('/llms.txt', (_req, res) => {res.type('text/plain').set('Cache-Control', TEXT).send(llmsTxt({name: SITE_NAME,++ base: site,summary: 'OpenVibe.Actor: an AI agent router and OpenVibe\'s own agent. A task in plain words goes to the best agent system for it, by what the task needs and the caller\'s mode (cheapest, balanced, best, fastest, private); a second model family checks the answer; the task records its cost to a fraction of a cent and why that agent was picked.',details: 'Agents today: the OpenVibe runtime (a DeepSeek model acting through OpenVibe.Search and OpenVibe.Tools: DNS, mail records, certificates, headers, WHOIS and more), OpenAI\'s web agent (live web search with sources), and an open model on OpenVibe\'s own server for private mode. Coding goes to OpenVibe.Codes. Every page is server-rendered and readable without JavaScript. The API: POST /api/v1/tasks, GET /api/v1/tasks/:id, GET /api/v1/tasks/:id/events (server-sent events), POST /api/v1/tasks/:id/cancel, GET /api/v1/agents and POST /api/v1/route (public dry run). Tasks are platform.task@1 in openvibe-contracts.',sections: [@@@ -97,17 +109,17 @@ function createDiscoveryRoutes(ctx) {{ title: 'The agents (JSON)', url: abs('/api/v1/agents'), note: 'actor.agent-list-result@2' },{ title: 'The agent manifest (JSON)', url: abs('/.well-known/agent.json'), note: 'what this site tells an agent about itself' },{ title: 'The free tier (JSON)', url: abs('/limits.json') },-- { title: 'Sitemap', url: abs('/sitemap.xml') },-- { title: 'Full text for language models', url: abs('/llms-full.txt') },-- { title: 'Release metadata (JSON)', url: abs('/release.json') },], notes: ['POST /api/v1/route — dry-run the router: send { task, mode? } and get which agent would take it and why (platform.placement-result@1). It answers POST only, so there is no page here to fetch; the API is described at /docs.',] },-- { title: 'Elsewhere', links: [-- { title: 'OpenVibe.Codes', url: 'https://openvibe.codes', note: 'the coding-agent harness Actor sends coding work to' },-- { title: 'OpenVibe.Tools', url: 'https://openvibe.tools', note: 'the tools the OpenVibe runtime uses' },-- { title: 'OpenVibe.Services', url: 'https://openvibe.services', note: 'apps, keys and actor.task.* grants' },-- ] },++ ],++ optional: [++ { title: 'Sitemap', url: abs('/sitemap.xml') },++ { title: 'Full text for language models', url: abs('/llms-full.txt') },++ { title: 'Release metadata (JSON)', url: abs('/release.json') },++ { title: 'OpenVibe.Codes', url: 'https://openvibe.codes', note: 'the coding-agent harness Actor sends coding work to' },++ { title: 'OpenVibe.Tools', url: 'https://openvibe.tools', note: 'the tools the OpenVibe runtime uses' },++ { title: 'OpenVibe.Services', url: 'https://openvibe.services', note: 'apps, keys and actor.task.* grants' },],}));});@@@ -48,8 +48,12 @@ const PRIVATE = ['/tasks', '/auth/', '/api/', '/limits.json'];assert.match(r.headers.get('content-type'), /^text\/plain/);assert.match(r.headers.get('cache-control') || '', /public, max-age=\d+/);assert.ok(r.text.startsWith('# OpenVibe.Actor'), 'no title');-- assert.ok(r.text.includes('(https://openvibe.actor/agents)'), 'no agents link');-- assert.ok(r.text.includes('(https://openvibe.actor/docs)'), 'no API link');++ // AGT-5: own pages are linked at their Markdown twins; Optional is the last section.++ assert.ok(r.text.includes('(https://openvibe.actor/agents.md)'), 'no agents link');++ assert.ok(r.text.includes('(https://openvibe.actor/docs.md)'), 'no API link');++ assert.ok(r.text.includes('(https://openvibe.actor/index.md)'), 'the home links its twin');++ assert.ok(r.text.includes('(https://openvibe.actor/api/v1/agents)') && r.text.includes('(https://openvibe.actor/limits.json)'), 'the API and files keep their URL');++ assert.ok(r.text.includes('\n## Optional\n') && r.text.lastIndexOf('\n## ') === r.text.indexOf('\n## Optional\n'), 'Optional is last');assert.ok(r.text.includes('(https://openvibe.services)'), 'no pointer to the developer platform');assert.ok(r.text.includes('https://openvibe.actor/sitemap.xml'), 'sitemap not listed');assert.ok(r.text.includes('(https://openvibe.actor/llms-full.txt)'), 'llms-full.txt not listed');
@@ -258,5 +258,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -258,5 +258,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.269.0
- openvibe-contracts: v0.269.0
- openvibe-sdk: v0.82.0
- openvibe-sdk: v0.82.0
-- openvibe-shared: v4.107.4
+- openvibe-shared: v4.108.0
<!-- versions:end -->
<!-- versions:end -->
@@ -9,11 +9,11 @@
@@ -9,11 +9,11 @@
"domain": "openvibe.actor",
"domain": "openvibe.actor",
"contracts": "openvibe-contracts v0.269.0",
"contracts": "openvibe-contracts v0.269.0",
"sdk": "openvibe-sdk v0.82.0",
"sdk": "openvibe-sdk v0.82.0",
- "shared": "openvibe-shared v4.107.4",
+ "shared": "openvibe-shared v4.108.0",
"packages": [
"packages": [
"openvibe-contracts v0.269.0",
"openvibe-contracts v0.269.0",
"openvibe-sdk v0.82.0",
"openvibe-sdk v0.82.0",
- "openvibe-shared v4.107.4"
+ "openvibe-shared v4.108.0"
],
],
"summary": "Public at https://openvibe.actor (sudo ovhost deploy actor; unit openvibe-actor on 127.0.0.1:4950). A task in plain words is classified, routed by openvibe-sdk/placement to the agent system that can do it (the OpenVibe runtime acting through OpenVibe.Search and OpenVibe.Tools, OpenAI's web agent, an open model on OpenVibe's server for private mode), run with its steps streamed live, checked by a model of another family, and delivered as platform.task@1 with its cost to a fraction of a cent and the explanation; a failed agent or check hands it to the next agent. Free tier per person with an operator ceiling.",
"summary": "Public at https://openvibe.actor (sudo ovhost deploy actor; unit openvibe-actor on 127.0.0.1:4950). A task in plain words is classified, routed by openvibe-sdk/placement to the agent system that can do it (the OpenVibe runtime acting through OpenVibe.Search and OpenVibe.Tools, OpenAI's web agent, an open model on OpenVibe's server for private mode), run with its steps streamed live, checked by a model of another family, and delivered as platform.task@1 with its cost to a fraction of a cent and the explanation; a failed agent or check hands it to the next agent. Free tier per person with an operator ceiling.",
"works": [
"works": [
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.269.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.269.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -1190,9 +1190,9 @@
@@ -1190,9 +1190,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.107.4",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
- "integrity": "sha512-tThN0m3KMncEHVcBOvE+ysHjsZTlVIDLG1P+/i2Mulmxfc/AUpebAXhpWWrut50OChZ8o+HeePxsSrPI8mUQOQ==",
+ "version": "4.108.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
+ "integrity": "sha512-NPzwjxpzGIpFpIm4uw3+VwHfsxbJ2+cD9yuhRzSwolSwqv2kCmwaVtHJ0DA9gKO3K8PCBfIP3UpKzpuSbYEJ+g==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -30,7 +30,7 @@
@@ -30,7 +30,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.269.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.269.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.107.4",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.108.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
@@ -8,6 +8,7 @@
@@ -8,6 +8,7 @@
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
const seo = require('openvibe-shared/seo');
const seo = require('openvibe-shared/seo');
+const { twinUrl } = require('openvibe-shared/agent-layer');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
const { asyncRouter } = require('./router');
const { asyncRouter } = require('./router');
@@ -52,12 +53,22 @@ const publicPages = () => [
@@ -52,12 +53,22 @@ const publicPages = () => [
* follows it gets nothing. So a section may carry notes: plain lines that describe such an
* 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.
* endpoint (POST /api/v1/route — ...) instead of linking it. Every real GET link stays a link.
*/
*/
-function llmsTxt({ name, summary, details, sections = [] }) {
+// AGT-5: each of the site's own pages is linked at its Markdown twin (/agents → /agents.md, / → /index.md); a file, an
+// /api/ or /.well-known/ path and another site's link keep their URL, as openvibe-shared's seo.llmsTxt markdown mode
+// does. `optional` is llmstxt.org's "## Optional", last.
+function twinOf(url, base) {
+ let u;
+ try { u = new URL(url); } catch { return url; }
+ if (!base || u.origin !== new URL(base).origin || /^\/(api|\.well-known)(\/|$)/.test(u.pathname) || /\.[a-z0-9]{1,8}$/i.test(u.pathname.split('/').pop())) return url;
+ return twinUrl(url);
+}
+
+function llmsTxt({ name, summary, details, sections = [], optional = [], base = null }) {
const out = [`# ${name}`, '', `> ${String(summary || '').replace(/\s+/g, ' ').trim()}`, ''];
const out = [`# ${name}`, '', `> ${String(summary || '').replace(/\s+/g, ' ').trim()}`, ''];
if (details) out.push(String(details).trim(), '');
if (details) out.push(String(details).trim(), '');
- for (const sec of sections) {
+ for (const sec of [...sections, ...(optional.length ? [{ title: 'Optional', links: optional }] : [])]) {
if (!sec) continue;
if (!sec) continue;
- const links = (sec.links || []).map((l) => '- [' + l.title + '](' + l.url + ')' + (l.note ? ': ' + String(l.note).replace(/\s+/g, ' ').trim() : ''));
+ const links = (sec.links || []).map((l) => '- [' + l.title + '](' + twinOf(l.url, base) + ')' + (l.note ? ': ' + String(l.note).replace(/\s+/g, ' ').trim() : ''));
const notes = (sec.notes || []).map((n) => String(n).replace(/\s+/g, ' ').trim());
const notes = (sec.notes || []).map((n) => String(n).replace(/\s+/g, ' ').trim());
if (!links.length && !notes.length) continue;
if (!links.length && !notes.length) continue;
out.push('## ' + sec.title, '');
out.push('## ' + sec.title, '');
@@ -84,6 +95,7 @@ function createDiscoveryRoutes(ctx) {
@@ -84,6 +95,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(llmsTxt({
res.type('text/plain').set('Cache-Control', TEXT).send(llmsTxt({
name: SITE_NAME,
name: SITE_NAME,
+ base: site,
summary: 'OpenVibe.Actor: an AI agent router and OpenVibe\'s own agent. A task in plain words goes to the best agent system for it, by what the task needs and the caller\'s mode (cheapest, balanced, best, fastest, private); a second model family checks the answer; the task records its cost to a fraction of a cent and why that agent was picked.',
summary: 'OpenVibe.Actor: an AI agent router and OpenVibe\'s own agent. A task in plain words goes to the best agent system for it, by what the task needs and the caller\'s mode (cheapest, balanced, best, fastest, private); a second model family checks the answer; the task records its cost to a fraction of a cent and why that agent was picked.',
details: 'Agents today: the OpenVibe runtime (a DeepSeek model acting through OpenVibe.Search and OpenVibe.Tools: DNS, mail records, certificates, headers, WHOIS and more), OpenAI\'s web agent (live web search with sources), and an open model on OpenVibe\'s own server for private mode. Coding goes to OpenVibe.Codes. Every page is server-rendered and readable without JavaScript. The API: POST /api/v1/tasks, GET /api/v1/tasks/:id, GET /api/v1/tasks/:id/events (server-sent events), POST /api/v1/tasks/:id/cancel, GET /api/v1/agents and POST /api/v1/route (public dry run). Tasks are platform.task@1 in openvibe-contracts.',
details: 'Agents today: the OpenVibe runtime (a DeepSeek model acting through OpenVibe.Search and OpenVibe.Tools: DNS, mail records, certificates, headers, WHOIS and more), OpenAI\'s web agent (live web search with sources), and an open model on OpenVibe\'s own server for private mode. Coding goes to OpenVibe.Codes. Every page is server-rendered and readable without JavaScript. The API: POST /api/v1/tasks, GET /api/v1/tasks/:id, GET /api/v1/tasks/:id/events (server-sent events), POST /api/v1/tasks/:id/cancel, GET /api/v1/agents and POST /api/v1/route (public dry run). Tasks are platform.task@1 in openvibe-contracts.',
sections: [
sections: [
@@ -97,17 +109,17 @@ function createDiscoveryRoutes(ctx) {
@@ -97,17 +109,17 @@ function createDiscoveryRoutes(ctx) {
{ title: 'The agents (JSON)', url: abs('/api/v1/agents'), note: 'actor.agent-list-result@2' },
{ title: 'The agents (JSON)', url: abs('/api/v1/agents'), note: 'actor.agent-list-result@2' },
{ title: 'The agent manifest (JSON)', url: abs('/.well-known/agent.json'), note: 'what this site tells an agent about itself' },
{ title: 'The agent manifest (JSON)', url: abs('/.well-known/agent.json'), note: 'what this site tells an agent about itself' },
{ title: 'The free tier (JSON)', url: abs('/limits.json') },
{ title: 'The free tier (JSON)', url: abs('/limits.json') },
- { title: 'Sitemap', url: abs('/sitemap.xml') },
- { title: 'Full text for language models', url: abs('/llms-full.txt') },
- { title: 'Release metadata (JSON)', url: abs('/release.json') },
], notes: [
], notes: [
'POST /api/v1/route — dry-run the router: send { task, mode? } and get which agent would take it and why (platform.placement-result@1). It answers POST only, so there is no page here to fetch; the API is described at /docs.',
'POST /api/v1/route — dry-run the router: send { task, mode? } and get which agent would take it and why (platform.placement-result@1). It answers POST only, so there is no page here to fetch; the API is described at /docs.',
] },
] },
- { title: 'Elsewhere', links: [
- { title: 'OpenVibe.Codes', url: 'https://openvibe.codes', note: 'the coding-agent harness Actor sends coding work to' },
- { title: 'OpenVibe.Tools', url: 'https://openvibe.tools', note: 'the tools the OpenVibe runtime uses' },
- { title: 'OpenVibe.Services', url: 'https://openvibe.services', note: 'apps, keys and actor.task.* grants' },
- ] },
+ ],
+ optional: [
+ { title: 'Sitemap', url: abs('/sitemap.xml') },
+ { title: 'Full text for language models', url: abs('/llms-full.txt') },
+ { title: 'Release metadata (JSON)', url: abs('/release.json') },
+ { title: 'OpenVibe.Codes', url: 'https://openvibe.codes', note: 'the coding-agent harness Actor sends coding work to' },
+ { title: 'OpenVibe.Tools', url: 'https://openvibe.tools', note: 'the tools the OpenVibe runtime uses' },
+ { title: 'OpenVibe.Services', url: 'https://openvibe.services', note: 'apps, keys and actor.task.* grants' },
],
],
}));
}));
});
});
@@ -48,8 +48,12 @@ const PRIVATE = ['/tasks', '/auth/', '/api/', '/limits.json'];
@@ -48,8 +48,12 @@ const PRIVATE = ['/tasks', '/auth/', '/api/', '/limits.json'];
assert.match(r.headers.get('content-type'), /^text\/plain/);
assert.match(r.headers.get('content-type'), /^text\/plain/);
assert.match(r.headers.get('cache-control') || '', /public, max-age=\d+/);
assert.match(r.headers.get('cache-control') || '', /public, max-age=\d+/);
assert.ok(r.text.startsWith('# OpenVibe.Actor'), 'no title');
assert.ok(r.text.startsWith('# OpenVibe.Actor'), 'no title');
- assert.ok(r.text.includes('(https://openvibe.actor/agents)'), 'no agents link');
- assert.ok(r.text.includes('(https://openvibe.actor/docs)'), 'no API link');
+ // AGT-5: own pages are linked at their Markdown twins; Optional is the last section.
+ assert.ok(r.text.includes('(https://openvibe.actor/agents.md)'), 'no agents link');
+ assert.ok(r.text.includes('(https://openvibe.actor/docs.md)'), 'no API link');
+ assert.ok(r.text.includes('(https://openvibe.actor/index.md)'), 'the home links its twin');
+ assert.ok(r.text.includes('(https://openvibe.actor/api/v1/agents)') && r.text.includes('(https://openvibe.actor/limits.json)'), 'the API and files keep their URL');
+ assert.ok(r.text.includes('\n## Optional\n') && r.text.lastIndexOf('\n## ') === r.text.indexOf('\n## Optional\n'), 'Optional is last');
assert.ok(r.text.includes('(https://openvibe.services)'), 'no pointer to the developer platform');
assert.ok(r.text.includes('(https://openvibe.services)'), 'no pointer to the developer platform');
assert.ok(r.text.includes('https://openvibe.actor/sitemap.xml'), 'sitemap not listed');
assert.ok(r.text.includes('https://openvibe.actor/sitemap.xml'), 'sitemap not listed');
assert.ok(r.text.includes('(https://openvibe.actor/llms-full.txt)'), 'llms-full.txt not listed');
assert.ok(r.text.includes('(https://openvibe.actor/llms-full.txt)'), 'llms-full.txt not listed');