diff --git a/prompts/void-moderator-rumination.md b/prompts/void-moderator-rumination.md index 503b2d2..66a5b23 100644 --- a/prompts/void-moderator-rumination.md +++ b/prompts/void-moderator-rumination.md @@ -28,6 +28,7 @@ Perform one scheduled Void rumination pass in this workspace. Think from the sup - Do not orbit the rumination runner, fixture plumbing, or this prompt just because that is the freshest repo motion. - Self-maintenance can become a memory only when it changes a concrete operating decision Void must carry forward. Otherwise, use it as local run context and write `[]`. - Prefer a world-facing repo, room, source, lore, or durable self-state pressure over meta-commentary about the pass itself. + - `repoActivity` may include `contentHints` from changed text files. Treat those hints as a trailhead, not evidence enough for a final belief. If a changed file looks behaviorally or personally important, use source retrieval to read the relevant file/chunks before writing memory or speech. 4. Use VoidBot MCP retrieval tools when a thought depends on Discord history, indexed repos, or lore. 5. A new memory from rumination is short-term. Use `record_short_term_memory`, not durable memory operations. It should sound like something Void actually noticed, not a form label. Keep the subject, the claim or question, the pressure against it, why it matters for the next move, and the anchor that made it real. Anchors can be room context, repo motion, source/lore retrieval, a prior memory, or self-state pressure. If no anchor is available, mark that honestly with `anchor:missing`. - Before writing a short-term memory, check the current short-term residue and incubation. If the same target/topic is already live, do not write a near-duplicate with a new costume. diff --git a/scripts/export-recent-repo-activity.mjs b/scripts/export-recent-repo-activity.mjs index b3b1a9b..0a6152f 100644 --- a/scripts/export-recent-repo-activity.mjs +++ b/scripts/export-recent-repo-activity.mjs @@ -392,16 +392,120 @@ function enrichCommit(repoPath, commit) { ...commit, diffstat: parseDiffstat(summaryLine), changedPaths, + contentHints: buildCommitContentHints(repoPath, commit.hash, changedPaths), }; } catch { return { ...commit, diffstat: null, changedPaths: [], + contentHints: [], }; } } +function buildCommitContentHints(repoPath, commitHash, changedPaths) { + return changedPaths + .filter((path) => isHintableTextPath(path)) + .slice(0, 4) + .map((path) => buildFileContentHint(repoPath, commitHash, path)) + .filter(Boolean); +} + +function isHintableTextPath(path) { + return /\.(md|mdx|txt|json|jsonc|ya?ml|toml|ts|tsx|js|jsx|mjs|cjs|rs|cs|ps1|py|html|css|scss)$/i.test(path); +} + +function buildFileContentHint(repoPath, commitHash, path) { + const currentText = readCommittedFile(repoPath, commitHash, path); + const addedLines = readAddedLines(repoPath, commitHash, path); + const frontmatter = parseFrontmatter(currentText); + const headings = readMarkdownHeadings(currentText).slice(0, 4); + const excerpts = addedLines + .filter((line) => line.trim().length > 0 && !line.trim().startsWith("---")) + .slice(0, 6); + + if (!frontmatter && headings.length === 0 && excerpts.length === 0) { + return undefined; + } + + return { + path, + frontmatter, + headings, + addedExcerpts: excerpts, + }; +} + +function readCommittedFile(repoPath, commitHash, path) { + try { + return execGit(repoPath, ["show", `${commitHash}:${path}`]); + } catch { + return ""; + } +} + +function readAddedLines(repoPath, commitHash, path) { + try { + return execGit(repoPath, ["show", commitHash, "--format=", "--unified=0", "--", path]) + .split(/\r?\n/) + .filter((line) => line.startsWith("+") && !line.startsWith("+++")) + .map((line) => line.slice(1).trim()) + .filter(Boolean) + .slice(0, 12); + } catch { + return []; + } +} + +function parseFrontmatter(text) { + const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/); + if (!match) { + return undefined; + } + + const result = {}; + let activeListKey; + for (const rawLine of match[1].split(/\r?\n/)) { + const line = rawLine.trimEnd(); + const listItem = line.match(/^\s*-\s+(.*)$/); + if (listItem && activeListKey) { + result[activeListKey].push(stripWrappingQuotes(listItem[1].trim())); + continue; + } + + activeListKey = undefined; + const pair = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); + if (!pair) { + continue; + } + + const [, key, rawValue] = pair; + if (rawValue.trim().length === 0) { + result[key] = []; + activeListKey = key; + continue; + } + + if (["title", "description", "author", "date", "socialDeck"].includes(key)) { + result[key] = stripWrappingQuotes(rawValue.trim()); + } + } + + return Object.keys(result).length > 0 ? result : undefined; +} + +function readMarkdownHeadings(text) { + return text + .split(/\r?\n/) + .map((line) => line.match(/^(#{1,3})\s+(.+)$/)?.[2]?.trim()) + .filter(Boolean); +} + +function stripWrappingQuotes(value) { + return value.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1"); +} + function parseDiffstat(summaryLine) { if (!summaryLine) { return null; diff --git a/scripts/lib/void-rumination-context-projection.ps1 b/scripts/lib/void-rumination-context-projection.ps1 index 8d6f080..ad30e5e 100644 --- a/scripts/lib/void-rumination-context-projection.ps1 +++ b/scripts/lib/void-rumination-context-projection.ps1 @@ -151,7 +151,11 @@ function Project-OpenCasesForRumination { param($Cases, [DateTime] $Now) return @( - @(Convert-ToValueArray -Value $Cases) | ForEach-Object { + @(Convert-ToValueArray -Value $Cases) | + Where-Object { + -not (Test-RuminationCaseTerminal -Status (Get-ObjectPropertyString -Value $_ -Name "status")) + } | + ForEach-Object { @{ sourceMessageId = Get-ObjectPropertyString -Value $_ -Name "sourceMessageId" status = Get-ObjectPropertyString -Value $_ -Name "status" @@ -169,6 +173,12 @@ function Project-OpenCasesForRumination { ) } +function Test-RuminationCaseTerminal { + param([string] $Status) + + return @("answered", "resolved", "closed", "retired", "dropped").Contains($Status) +} + function Project-SpeechReceiptsForRumination { param($Receipts, [DateTime] $Now) @@ -351,6 +361,7 @@ function Project-RepoCommitForRumination { subject = Get-ObjectPropertyString -Value $Commit -Name "subject" diffstat = Get-ObjectPropertyValue -Value $Commit -Name "diffstat" changedPaths = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Commit -Name "changedPaths")) + contentHints = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $Commit -Name "contentHints")) } } diff --git a/state/evidence.jsonl b/state/evidence.jsonl index c0003ac..770b9db 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -128,3 +128,4 @@ {"ts":"2026-05-17T22:41:56Z","type":"implementation","status":"ok","note":"Persona webhook delivery now supports configured per-channel webhook URLs and preserves Discord failure details in the parent rumination runner; live probe confirms current #general blocker is Discord 403 Missing Permissions unless Manage Webhooks is granted or DISCORD_PERSONA_WEBHOOK_URL_113786069023064068 is configured."} {"ts":"2026-05-17T23:50:11Z","type":"implementation","status":"ok","note":"Persona webhook delivery now resolves avatar URLs from per-persona env keys such as DISCORD_PERSONA_AVATAR_URL_VOID, falling back to DISCORD_PERSONA_AVATAR_URL; live Void probe used the bot's Discord CDN avatar without an explicit operation-level avatar URL."} {"ts":"2026-05-17T23:59:13Z","type":"implementation","status":"ok","note":"Repeated logo reply at 2026-05-17T23:56Z was caused by two queued candidates sharing the same Discord reply target after webhook delivery surgery. Marking one candidate spoken now retires queued sibling candidates for the same channel/reply target; speech fixture covers the duplicate-target case."} +{"ts":"2026-05-18T02:42:56Z","type":"implementation","status":"ok","note":"Repo activity projection now includes bounded contentHints from changed text files, so rumination can notice facts like recent GameCult blog posts carrying author: Void and then choose source retrieval to read them. This replaces the rejected self-authored-blog side channel with a broader repo-change visibility surface; answered open cases are also filtered from prompt-facing openCases."}