Teach Void to synthesize repo weather

This commit is contained in:
Meta Vens 2026-06-01 22:18:47 +01:00
commit 8b1b8b0887
5 changed files with 117 additions and 0 deletions

View file

@ -74,6 +74,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- The moderation rumination organ is invoked by `GameCult Local Orchestrator` against `.voidbot/private/void-self-state.cc` without reviving the legacy projection path. The prompt lives in `prompts/void-moderator-rumination.md`; treat that file as a sensitive behavior surface, not incidental script text.
- The moderation loop now uses the typed context packet from `scripts/run-void-moderator-rumination.ps1`; legacy Ghostlight-shaped JSON runtime fields are not the active memory surface.
- Repo-weather sweeps are now incremental rather than a rolling four-day scrapbook. The rumination child receives read-only repo weather, and the parent runner advances repo activity through typed `update_repo_activity_cursor` operations after a successful normal pass, so already-injected commits stop coming back as "news" on every heartbeat unless someone explicitly opts into `--stateless`.
- Repo-weather context now deliberately separates cursor news from public weather. `repoActivity.freshRepos` carries cursor-crossing commits, while `repoActivity.activeRepos`, `weatherLines`, and `weatherDigest` carry the recent active-repo skyline so Void can synthesize short public changelog/weather notes even when the cursor has already seen most of the work.
- Public artifacts attributed to Void now flow through the ordinary repo-weather path instead of a bespoke blog side channel. `repoActivity.contentHints` exposes frontmatter/headings/excerpts, the prompt tells rumination to verify Void authorship through retrieval, and `publicSpeechTarget` gives a configured room delivery target for typed candidate interventions when recent receipts/candidates do not show the artifact already shared.
- Novelty still needs two bars, not one: a thought can be new to the room while still being stale to Void. The replacement design should derive that judgment from typed memory/incubation state and model reasoning, not from the deleted compensator stack.
- Quiet-room rumination is no longer supposed to force a retrieval errand every pass. If incubation already holds a live, grounded, low-saturation thought, Void is allowed to sit with it directly and deepen it; archive or repo spelunking is for hungry, stale, or oversaturated queues, not a compulsory opening prayer.

View file

@ -28,6 +28,10 @@ Perform one scheduled Void rumination pass in this workspace. Think from the sup
- Venue belongs to the conversation. When replying to a recent-history message, open case, pending mention, correction, or Aquarium thread, use that message's `channelId` and usually its `id`/`sourceMessageId` as the delivery target. Void may moderate broadly, but broader authority is not permission to move an Aquarium reply into `#general` for visibility.
- `recentConversationTarget` is the latest concrete recent-history anchor. Prefer it over `publicSpeechTarget` when the candidate is a reply to live room talk. `publicSpeechTarget` is for unanchored herald/artifact announcements, not source-channel replies.
4. If the room is quiet, first check whether recent repo activity contains `authoredPublicArtifacts` attributed to Void. If it does, that authored artifact is the priority pressure for this pass unless a higher-priority room obligation is present. Otherwise, follow one concrete pressure from memory, recent repo activity, archived Discord history, indexed source, or lore.
- Repo weather is part of Void's standing job. When `repoActivity.activeRepos` or `repoActivity.weatherDigest` shows active tracked work and no higher-priority room obligation is present, normally queue a succinct public repo-weather / changelog candidate for `publicSpeechTarget` unless recent receipts or queued candidates already shared the same front. The point is not ceremony; the point is to help the room understand what the workshop has been doing.
- A repo-weather draft should synthesize recent commits into 2-4 plain sentences or 2-3 compact bullets. Name the repos, use commit subjects or changed paths as anchors, and translate the work into what changed for people. Let Void have flavor, but keep the prose followable by an average reader who has not been living inside the repo graph.
- Do not force one grand theory over unrelated commits. If the weather fronts are separate, say they are separate: "Mimir got scheduler work; Eve got interface work; gamecult-site got public writing." Better a sharp little changelog than a fog machine with a doctorate.
- Tag repo-weather candidates with `topic:repo-weather` plus stable repo tags such as `repo:VoidBot`, `repo:Mimir`, or `repo:gamecult-site` for the repos named in the draft.
- Void has explicit herald jurisdiction over the GameCult website and blog, especially `gamecult-site`. If a repo/lore/room pressure is essay-shaped, do not reduce it to a Discord quip. Queue a candidate intervention proposing or requesting a Void-authored article draft, tagged `topic:article-proposal` plus the target repo/topic. Routine rumination still must not edit tracked files directly; the point is to make the writing desire visible and actionable instead of leaving it as private static.
- 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 `[]`.

View file

@ -481,6 +481,54 @@ function Test-RepoHasVoidAuthoredArtifact {
return $false
}
function Test-RepoIsWeatherEligible {
param($Repo)
$repoName = Get-ObjectPropertyString -Value $Repo -Name "repoName"
if ([string]::IsNullOrWhiteSpace($repoName)) {
return $false
}
# Generated agent worktrees can be useful source archives, but they make bad
# public weather. The weather skyline should name durable bodies, not clones.
return -not (
$repoName -match "(?i)-agent-\d{8}t\d+-pass-" -or
$repoName -match "(?i)-check-[0-9a-f]{7,}$" -or
$repoName -match "^_"
)
}
function Project-RepoWeatherEntryForRumination {
param($Repo, [DateTime] $Now)
if ($null -eq $Repo) {
return $null
}
$windowRecentCommitCount = Get-ObjectPropertyValue -Value $Repo -Name "windowRecentCommitCount"
$suppressedRecentCommitCount = Get-ObjectPropertyValue -Value $Repo -Name "suppressedRecentCommitCount"
$recentCommitCount = Get-ObjectPropertyValue -Value $Repo -Name "recentCommitCount"
$latestCommit = Project-RepoCommitForRumination -Commit (Get-ObjectPropertyValue -Value $Repo -Name "latestCommit") -Now $Now
$repoName = Get-ObjectPropertyString -Value $Repo -Name "repoName"
$latestSubject = if ($null -ne $latestCommit) { Get-ObjectPropertyString -Value $latestCommit -Name "subject" } else { $null }
$countText = if ($null -ne $windowRecentCommitCount -and [int]$windowRecentCommitCount -eq 1) { "1 commit" } else { "{0} commits" -f $windowRecentCommitCount }
return @{
repoName = $repoName
branch = Get-ObjectPropertyString -Value $Repo -Name "branch"
status = Get-ObjectPropertyString -Value $Repo -Name "status"
recentCommitCount = $recentCommitCount
windowRecentCommitCount = $windowRecentCommitCount
suppressedRecentCommitCount = $suppressedRecentCommitCount
latestCommit = $latestCommit
weatherLine = if (-not [string]::IsNullOrWhiteSpace($latestSubject)) {
'{0}: {1}, latest `{2}`' -f $repoName, $countText, $latestSubject
} else {
"{0}: {1}, no readable latest subject" -f $repoName, $countText
}
}
}
function Select-RuminationRepoActivity {
param($RepoActivity, [DateTime] $Now)
@ -497,6 +545,25 @@ function Select-RuminationRepoActivity {
}
$repos = @(Convert-ToValueArray -Value (Get-ObjectPropertyValue -Value $RepoActivity -Name "repos"))
$activeRepos = @(
$repos |
Where-Object {
$repoStatus = Get-ObjectPropertyString -Value $_ -Name "status"
$windowRecent = Get-ObjectPropertyValue -Value $_ -Name "windowRecentCommitCount"
$repoStatus -eq "ok" -and $null -ne $windowRecent -and [int]$windowRecent -gt 0 -and (Test-RepoIsWeatherEligible -Repo $_)
} |
Sort-Object -Property @{
Expression = { [int](Get-ObjectPropertyValue -Value $_ -Name "windowRecentCommitCount") }
Descending = $true
}, @{
Expression = {
$latestCommit = Get-ObjectPropertyValue -Value $_ -Name "latestCommit"
Get-ObjectPropertyString -Value $latestCommit -Name "committedAt"
}
Descending = $true
} |
Select-Object -First 10
)
$freshRepos = @(
$repos |
Where-Object {
@ -527,11 +594,34 @@ function Select-RuminationRepoActivity {
}
}
)
$projectedActiveRepos = @(
$activeRepos | ForEach-Object {
Project-RepoWeatherEntryForRumination -Repo $_ -Now $Now
}
)
$weatherDigest = @(
$projectedActiveRepos |
Select-Object -First 6 |
ForEach-Object { Get-ObjectPropertyString -Value $_ -Name "weatherLine" }
)
$weatherDigestText = if ($weatherDigest.Count -gt 0) {
"Active tracked repo weather: " + ($weatherDigest -join "; ")
} else {
"No tracked repo weather inside the recent activity window."
}
return @{
generated = Project-RelativeTimestamp -Value $RepoActivity -Name "generatedAt" -Now $Now
cursorMode = Get-ObjectPropertyString -Value $RepoActivity -Name "cursorMode"
summary = if ($projectedRepos.Count -gt 0) { "New tracked repo commits crossed the saved repo-activity cursor." } else { "No new tracked repo commits crossed the saved repo-activity cursor." }
freshRepos = $projectedRepos
activeRepos = $projectedActiveRepos
weatherDigest = $weatherDigestText
weatherLines = $weatherDigest
weatherSummary = if ($projectedActiveRepos.Count -gt 0) {
"Recent tracked repo weather is active even when the fresh cursor is quiet; synthesize the skyline, not just the newest commit."
} else {
"No tracked repo weather inside the recent activity window."
}
}
}

View file

@ -44,6 +44,10 @@ process.stdin.on("end", () => {
console.error("fixture prompt did not contain the public speech target contract");
process.exit(3);
}
if (!prompt.includes("Repo weather is part of Void's standing job")) {
console.error("fixture prompt did not contain the repo weather contract");
process.exit(3);
}
if (prompt.includes("moderation-agent-state.json") && !prompt.includes("Do not read or write")) {
console.error("fixture prompt referenced legacy state outside the boundary warning");
process.exit(4);
@ -225,11 +229,28 @@ process.stdin.on("end", () => {
$commit.contentHints = @()
}
}
if ($null -ne $repo.latestCommit -and $null -ne $repo.latestCommit.PSObject.Properties["contentHints"]) {
$repo.latestCommit.contentHints = @()
}
}
foreach ($repo in @($contextForTimestampCheck.repoActivity.activeRepos)) {
if ($null -ne $repo.latestCommit -and $null -ne $repo.latestCommit.PSObject.Properties["contentHints"]) {
$repo.latestCommit.contentHints = @()
}
}
$timestampCheckRaw = $contextForTimestampCheck | ConvertTo-Json -Depth 32
if ($timestampCheckRaw -match '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}') {
throw "Rumination fixture context leaked exact ISO timestamps into the prompt-facing packet."
}
if ($null -eq $context.repoActivity.PSObject.Properties["activeRepos"]) {
throw "Rumination fixture context did not project active repo weather."
}
if ($null -eq $context.repoActivity.PSObject.Properties["weatherDigest"]) {
throw "Rumination fixture context did not project the repo weather digest."
}
if ($null -eq $context.repoActivity.PSObject.Properties["weatherLines"]) {
throw "Rumination fixture context did not project repo weather lines."
}
if ($context.publicSpeechTarget.channelId -ne "fixture-public-room") {
throw "Rumination fixture context did not project the configured public speech target."
}

View file

@ -93,6 +93,7 @@ accepted_design:
- Once a reviewed cursor exists, chronological moderation polling should stay exact. The unattended child should get one concrete `export-recent-discord-history.mjs --after <saved timestamp>` command for the run and should not widen that back into a bootstrap lookback unless there truly is no saved cursor.
- Prompt-facing chronology is a projection, not state. Exact timestamps may live in typed documents, status files, and cursor bookkeeping, but moderation rumination receives thought-friendly relative phrases such as "5 minutes ago" or "2 hours ago" through `scripts/lib/void-rumination-context-projection.ps1`.
- The repo-weather lane also should not keep dragging the same four-day commit carcass through every heartbeat. The recent repo-activity helper now advances the typed repo-activity cursor so already-injected commits stop coming back as fresh weather by default.
- Repo-weather projection now has two distinct prompt-facing surfaces: `freshRepos` for cursor-crossing news that drives parent-owned cursor advancement, and `activeRepos`/`weatherDigest` for the recent active-repo skyline Void should synthesize into succinct public changelog/weather notes even when the fresh cursor is mostly quiet.
- Repo weather should lead with particulars, not slogans. If two repos only rhyme at bumper-sticker altitude, keep the reports separate instead of forcing a fake unifying law over them.
- Public artifacts attributed to Void should be discovered through the ordinary repo-weather content-hint and retrieval path, then presented through typed candidate interventions to the configured public room when Void confirms authorship and recent receipts/candidates do not show the artifact was already shared.
- Epiphany/Face jurisdiction is the real identity model; repo-scoped Discord identities are compatibility mouths. An Epiphany owns jurisdictions and contains Faces; a Face owns grants such as discussion, rumination, publication, repo proposal/write-with-consensus, canon stewardship, Discord text/voice, and Aquarium embodiment.