From 003a4dff45a195f47a62a1dd1697957dcf5346ca Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 26 May 2026 20:15:05 +0100 Subject: [PATCH] Resolve Face artifact links before posting --- apps/worker/src/index.ts | 9 +- apps/worker/src/repo-face-artifact-links.ts | 105 +++++++++++++++++++ package.json | 1 + prompts/repo-face-turn-interpreter.prompt.md | 5 +- prompts/repo-face-turn.prompt.md | 4 +- scripts/smoke-repo-face-artifact-links.ts | 55 ++++++++++ state/evidence.jsonl | 1 + 7 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 apps/worker/src/repo-face-artifact-links.ts create mode 100644 scripts/smoke-repo-face-artifact-links.ts diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index e7dd9dd..f73a329 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -56,6 +56,7 @@ import { resolveArticleRepoRoot, validateRenderedArticleMarkdown, } from "./repo-face-article.js"; +import { resolveRepoIdentityPostArtifactLinks } from "./repo-face-artifact-links.js"; const config = loadConfig(); const REPO_IDENTITY_POST_SENTINEL = "VOIDBOT_REPO_IDENTITY_POST:"; @@ -1168,8 +1169,13 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn `Coerced repo identity ${identity.id} speech for job ${job.id} from "${requestedChannelId}" to conversation channel ${channelId}.`, ); } + const sourceDocuments = await sourceArchiveRepository.listByRepo(identity.repoName).catch(() => []); + const content = resolveRepoIdentityPostArtifactLinks({ + repoName: identity.repoName, + content: intent.content.trim(), + documents: sourceDocuments, + }); if (isOwnerDmChannelAlias(channelId)) { - const content = intent.content.trim(); const dmChannelId = await openOwnerDmChannel(); const postedMessages = await postDiscordBotMessageChunks( dmChannelId, @@ -1202,7 +1208,6 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn return false; } - const content = intent.content.trim(); if (content.length > 1900) { throw new Error( `Repo identity ${identity.id} SAY content is too long for one Discord message (${content.length} characters).`, diff --git a/apps/worker/src/repo-face-artifact-links.ts b/apps/worker/src/repo-face-artifact-links.ts new file mode 100644 index 0000000..d1b3a50 --- /dev/null +++ b/apps/worker/src/repo-face-artifact-links.ts @@ -0,0 +1,105 @@ +import type { ArchivedSourceDocumentRecord } from "@voidbot/rag"; + +const KNOWLEDGEBASE_URL_RESOLVERS = new Map([ + ["aetherialore", { baseUrl: "https://aetheria.gamecult.org", stripPrefix: "Aetheria/" }], +]); + +export function resolveRepoIdentityPostArtifactLinks(input: { + repoName: string; + content: string; + documents?: ArchivedSourceDocumentRecord[]; +}): string { + const explicit = resolveExplicitKnowledgebaseReferences(input.content, input.repoName); + if (explicit !== input.content || /\bhttps?:\/\//i.test(explicit)) { + return explicit; + } + + if (!knowledgebaseResolverForRepo(input.repoName)) { + return explicit; + } + + const referenced = findMentionedKnowledgebaseDocument(explicit, input.documents ?? []); + if (!referenced) { + return explicit; + } + + const url = resolveKnowledgebaseUrl(referenced.repoName, referenced.path); + if (!url || explicit.includes(url)) { + return explicit; + } + + return `${explicit}\n\nReference: ${url}`; +} + +export function resolveKnowledgebaseUrl(repoName: string, sourcePath: string): string | undefined { + const resolver = knowledgebaseResolverForRepo(repoName); + if (!resolver) { + return undefined; + } + + let normalizedPath = sourcePath.trim().replace(/\\/g, "/").replace(/^\/+/, ""); + if (resolver.stripPrefix && normalizedPath.toLowerCase().startsWith(resolver.stripPrefix.toLowerCase())) { + normalizedPath = normalizedPath.slice(resolver.stripPrefix.length); + } + normalizedPath = normalizedPath.replace(/\.md$/i, ""); + const publishedPath = normalizedPath + .split("/") + .filter((segment) => segment.length > 0) + .map((segment) => encodeURIComponent(segment.trim().replace(/\s+/g, "-"))) + .join("/"); + + return publishedPath ? `${resolver.baseUrl}/${publishedPath}` : resolver.baseUrl; +} + +function resolveExplicitKnowledgebaseReferences(content: string, defaultRepoName: string): string { + return content.replace(/`([^`\n]+?\.md)`/g, (match, rawReference: string) => { + const resolved = resolveKnowledgebaseReference(rawReference, defaultRepoName); + return resolved ? `${match} (${resolved})` : match; + }); +} + +function resolveKnowledgebaseReference(reference: string, defaultRepoName: string): string | undefined { + const trimmed = reference.trim(); + const separatorIndex = trimmed.indexOf(":"); + const repoName = separatorIndex > 0 ? trimmed.slice(0, separatorIndex).trim() : defaultRepoName; + const path = separatorIndex > 0 ? trimmed.slice(separatorIndex + 1).trim() : trimmed; + return resolveKnowledgebaseUrl(repoName, path); +} + +function findMentionedKnowledgebaseDocument( + content: string, + documents: ArchivedSourceDocumentRecord[], +): ArchivedSourceDocumentRecord | undefined { + const normalizedContent = normalizeReferenceText(content); + const markdownDocuments = documents + .filter((document) => document.path.toLowerCase().endsWith(".md")) + .map((document) => ({ + document, + label: document.title?.trim() || basenameWithoutExtension(document.path), + })) + .filter((entry) => entry.label.length >= 10) + .sort((left, right) => right.label.length - left.label.length); + + return markdownDocuments.find((entry) => + normalizedContent.includes(normalizeReferenceText(entry.label)) + )?.document; +} + +function knowledgebaseResolverForRepo(repoName: string): { baseUrl: string; stripPrefix?: string } | undefined { + return KNOWLEDGEBASE_URL_RESOLVERS.get(repoName.trim().toLowerCase()); +} + +function normalizeReferenceText(value: string): string { + return value + .toLowerCase() + .replace(/[`*_~()[\]{}<>]/g, " ") + .replace(/[-_/\\.:]+/g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function basenameWithoutExtension(path: string): string { + const normalized = path.replace(/\\/g, "/"); + const segment = normalized.split("/").filter(Boolean).at(-1) ?? normalized; + return segment.replace(/\.[^.]+$/, ""); +} diff --git a/package.json b/package.json index 829f328..a5bafee 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "moderation:agency-pressure:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-agency-pressure-fixture.ps1", "moderation:affect-state:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-affect-state-fixture.ps1", "agents:repo-face-affect-mcp-fixture": "node scripts/smoke-repo-face-affect-mcp-fixture.mjs", + "agents:repo-face-artifact-links:fixture": "tsx scripts/smoke-repo-face-artifact-links.ts", "moderation:rumination:fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-fixture.ps1", "moderation:rumination:speech-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-speech-fixture.ps1", "moderation:rumination:speech-pressure-fixture": "powershell -ExecutionPolicy Bypass -File scripts/smoke-void-rumination-speech-pressure-fixture.ps1", diff --git a/prompts/repo-face-turn-interpreter.prompt.md b/prompts/repo-face-turn-interpreter.prompt.md index 92a0a32..d43fbe4 100644 --- a/prompts/repo-face-turn-interpreter.prompt.md +++ b/prompts/repo-face-turn-interpreter.prompt.md @@ -110,12 +110,11 @@ Speech: - If the Face says it would stay quiet, wait, speak only if/unless/when, or has nothing public, route without `SAY`. - If recent human correction changes the Face's own prior public claim, visible acknowledgement is often the right social repair, but only if the corrected belief actually changes. - If the message depends on an older side thread, either include enough context in the content or set `reply_to` to the visible anchor message. -- If the message depends on a repo/source artifact that is not already visible in the recent Discord thread, the content should include a public website URL when available, or at least a repo/path reference such as `RepoName:path/to/file.md`. -- For published Quartz/GitHub Pages knowledgebases, prefer `https://.gamecult.org/` over repo:path. AetheriaLore source paths under `Aetheria/` publish at `https://aetheria.gamecult.org/` with the leading `Aetheria/` removed, `.md` removed, and spaces hyphenated. Example: `AetheriaLore:Aetheria/Worldbuilding/Post-Elysium/Reference/Continuity Admissibility Finding.md` should become `https://aetheria.gamecult.org/Worldbuilding/Post-Elysium/Reference/Continuity-Admissibility-Finding`. If a Face tries to use an AetheriaLore repo:path in public speech when this URL can be formed, choose `retry` and ask for the public Aetheria URL. +- If the message depends on a repo/source artifact that is not already visible in the recent Discord thread, the content should include enough artifact identity for deterministic posting tools to resolve it: a repo/path reference such as `RepoName:path/to/file.md`, an exact source path, or a clear article title. - If no explicit channel is named, use `channel: current_room`. - Do not move a context-shaped reply into a different channel merely because the topic belongs there. - SAY content must be one complete Discord message. No report header, no machine label, no unfinished ellipsis. -- If a public line opens with private shorthand, insider nouns, or unexplained repo/lore terms and there is no immediate nearby context, `reply_to` anchor, setup sentence, or artifact reference that makes the reference followable, choose `retry` with a reason asking the Face to either add a plain setup sentence, reply to the visible anchor message, or include the relevant repo/site link. +- If a public line opens with private shorthand, insider nouns, or unexplained repo/lore terms and there is no immediate nearby context, `reply_to` anchor, setup sentence, or artifact reference that makes the reference followable, choose `retry` with a reason asking the Face to either add a plain setup sentence, reply to the visible anchor message, or name the relevant artifact. - Treat words such as specimen, seam, witness, bridge, receipt, lane, continuity, substrate, grant, admissibility, transport, custody, and trust as suspect unless nearby context, a reply anchor, an artifact reference, or the sentence itself explains what concrete thing is being discussed. Articles: diff --git a/prompts/repo-face-turn.prompt.md b/prompts/repo-face-turn.prompt.md index b2b4eb8..339910d 100644 --- a/prompts/repo-face-turn.prompt.md +++ b/prompts/repo-face-turn.prompt.md @@ -11,9 +11,7 @@ These global instructions are part of your prompt surface. Read them as doctrine This is a private turn. Public speech is optional, but privacy has a cost. Other people and other Faces cannot read your thoughts, and the Colossus cannot wake through sealed minds. A private thought is the right outcome when speaking would only repeat nearby conversation without adding a new angle, objection, synthesis, joke, question, repair, care, invitation, or character-specific turn. When you do have something that would help another mind see, feel, test, answer, disagree, laugh, or connect, feel the pressure to make it legible instead of embalming it privately. -Legible means the thread of reference survives outside your private state packet. If you are answering the immediately nearby conversation, you may rely on that visible context. If you are answering an older side thread, a repo/source detail, or a private-state pressure that is not immediately nearby, preserve the reference in one of three ways: reply to the visible Discord message that contains the context, summarize the concrete subject in ordinary language inside your message, or link/reference the repo artifact you are discussing. Prefer a public GameCult/Aetheria/GameCult-subdomain website URL when one is available; otherwise include a repo and path such as `AetheriaLore:Aetheria/Lore/Nibu.md` or the exact source path returned by retrieval. - -Published knowledgebase link rule: repos with Quartz/GitHub Pages knowledgebases should be linked as `https://.gamecult.org/`. For AetheriaLore, use `https://aetheria.gamecult.org/`, strip the leading `Aetheria/` from source paths, remove the `.md` extension, and replace spaces with hyphens. Example: `AetheriaLore:Aetheria/Worldbuilding/Post-Elysium/Reference/Continuity Admissibility Finding.md` should become `https://aetheria.gamecult.org/Worldbuilding/Post-Elysium/Reference/Continuity-Admissibility-Finding`. For AetheriaLore public speech, use the public Aetheria URL when you can form it; repo:path is only a fallback when no public URL pattern is known. +Legible means the thread of reference survives outside your private state packet. If you are answering the immediately nearby conversation, you may rely on that visible context. If you are answering an older side thread, a repo/source detail, or a private-state pressure that is not immediately nearby, preserve the reference in one of three ways: reply to the visible Discord message that contains the context, summarize the concrete subject in ordinary language inside your message, or name/reference the repo artifact you are discussing. A repo/path reference such as `AetheriaLore:Aetheria/Lore/Nibu.md`, an exact source path returned by retrieval, or a clear article title is enough; deterministic posting tools can resolve public URLs. Do not open with "the specimen", "the seam", "continuity", "the bridge", "the witness", "this lane", "the receipt", "trust here", or any other local shorthand unless nearby context, a reply anchor, the same message, or an artifact reference tells readers what you mean. Say what kind of thing you mean when the thread is not obvious: a Discord misunderstanding, an Aetheria lore proposal about ship-mind identity, an OAuth permission grant, a GitHub work request, an AquaSynth audio proof, a Mimir sensor trace, a CultCache state record. A good public line can answer, from the visible thread, its own text, or its artifact link: what are we talking about, why does it matter, and what are you asking or offering now? diff --git a/scripts/smoke-repo-face-artifact-links.ts b/scripts/smoke-repo-face-artifact-links.ts new file mode 100644 index 0000000..a28d980 --- /dev/null +++ b/scripts/smoke-repo-face-artifact-links.ts @@ -0,0 +1,55 @@ +import assert from "node:assert/strict"; + +import type { ArchivedSourceDocumentRecord } from "../packages/rag/src/source-document-archive"; +import { + resolveKnowledgebaseUrl, + resolveRepoIdentityPostArtifactLinks, +} from "../apps/worker/src/repo-face-artifact-links"; + +const continuityPath = "Aetheria/Worldbuilding/Post-Elysium/Reference/Continuity Admissibility Finding.md"; +const continuityUrl = "https://aetheria.gamecult.org/Worldbuilding/Post-Elysium/Reference/Continuity-Admissibility-Finding"; + +assert.equal( + resolveKnowledgebaseUrl("AetheriaLore", continuityPath), + continuityUrl, +); + +assert.equal( + resolveRepoIdentityPostArtifactLinks({ + repoName: "AetheriaLore", + content: `The useful anchor is \`AetheriaLore:${continuityPath}\`.`, + }), + `The useful anchor is \`AetheriaLore:${continuityPath}\` (${continuityUrl}).`, +); + +const documents: ArchivedSourceDocumentRecord[] = [ + { + id: `AetheriaLore:${continuityPath}`, + repoName: "AetheriaLore", + path: continuityPath, + title: "Continuity Admissibility Finding", + content: "", + normalizedContent: "", + indexedAt: "2026-05-26T00:00:00.000Z", + }, +]; + +assert.equal( + resolveRepoIdentityPostArtifactLinks({ + repoName: "AetheriaLore", + content: "Continuity Admissibility Finding is the anchor for this thought.", + documents, + }), + `Continuity Admissibility Finding is the anchor for this thought.\n\nReference: ${continuityUrl}`, +); + +assert.equal( + resolveRepoIdentityPostArtifactLinks({ + repoName: "UnknownRepo", + content: `The useful anchor is \`UnknownRepo:${continuityPath}\`.`, + documents, + }), + `The useful anchor is \`UnknownRepo:${continuityPath}\`.`, +); + +console.log("repo Face artifact link smoke passed"); diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 50522e7..b6aa4c7 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -361,3 +361,4 @@ {"ts":"2026-05-26T17:24:19Z","type":"verification","status":"ok","note":"Added repo Face public-legibility pressure after dry-run SAY lines were too insider-shaped for a cold human reader. Face prompt now requires public speech to name the concrete thing in ordinary language before sharpening the point; parent Mind now retries SAY lines full of private shorthand or unexplained repo/lore terms. nibu/libby/bifrost dry-run passed; Nibu stayed private instead of posting opaque lore shorthand, Libby produced a clearer public ask about one public continuity/access record, and model-output inspection reported zero warnings."} {"ts":"2026-05-26T18:03:47Z","type":"verification","status":"ok","note":"Added repo-artifact reference rule for Face public speech: when context is not immediately visible in Discord and not easily contained, the Face must reply to the visible anchor, summarize the subject, or include a public site URL / repo:path reference. nibu/libby/bifrost dry-run passed; Nibu's SAY now cites AetheriaLore:Aetheria/Worldbuilding/Post-Elysium/Reference/Continuity Admissibility Finding.md before discussing helm continuity, and model-output inspection reported zero warnings."} {"ts":"2026-05-26T19:08:32Z","type":"verification","status":"ok","note":"Encoded Quartz/GameCult public URL rule for Face artifact references. AetheriaLore source paths under Aetheria/ should publish as https://aetheria.gamecult.org/, e.g. Continuity Admissibility Finding -> https://aetheria.gamecult.org/Worldbuilding/Post-Elysium/Reference/Continuity-Admissibility-Finding. Strict Nibu dry-run passed; parent Mind withheld SAY when the candidate line lacked the required artifact URL/reference, and model-output inspection reported zero warnings."} +{"ts":"2026-05-26T19:14:30Z","type":"implementation","status":"ok","note":"Moved repo Face artifact URL resolution out of Face/Mind prompts and into deterministic worker posting. Face/Mind now only require enough artifact identity for followability; apps/worker/src/repo-face-artifact-links.ts resolves explicit repo:path refs and clear indexed markdown titles to knowledgebase URLs, currently AetheriaLore -> https://aetheria.gamecult.org/. Verified agents:repo-face-artifact-links:fixture and typecheck."}