diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 5685dc8..d1bdfea 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -11,6 +11,7 @@ import { buildVoidMcpServerConfig, createStateStorage, applyVoidSelfStateOperation, + findCrossRepoJurisdictionMentions, findRepoDiscordIdentity, isRepoDiscordIdentityAllowedInChannel, type JobQueue, @@ -973,6 +974,21 @@ async function enqueueRepoIdentityUpdateRequestIntent( throw new Error(`No registered repo identity matched "${identityId}" for update request intent on job ${job.id}.`); } + const crossRepoMentions = findCrossRepoJurisdictionMentions( + identity, + registry, + `${intent.title}\n${intent.content}`, + ); + if (crossRepoMentions.length > 0) { + const conflicts = crossRepoMentions + .map((match) => `${match.repoName} (${match.identityId}, matched "${match.matched}")`) + .join(", "); + throw new Error( + `Repo Face UPDATE REQUEST from ${identity.id}/${identity.repoName} crosses registered jurisdiction: ${conflicts}. ` + + "Immediate UPDATE REQUEST is repo-local; use BIFROST TOPIC handoff/consultation or let the owning Face dispatch the work.", + ); + } + const contentFile = await writeBifrostPayloadFile( job, `${identity.id}-bifrost-update-request.md`, diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 8063a3f..911fdea 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -122,7 +122,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Repo Face identity is now seeded into the typed `.cc` self profile during initialization and passed through repo Face MCP state reads/writes. The renderer may still provide identity context, but raw state should not quietly say `Void` for a non-Void Face. - Repo Face prompts now also grant advisory repo-change agency: Faces may build long-running maps of their repos and propose concrete changes, but Discord jobs stay read-only and must ask for human consensus before implementation changes canonical repo material. Once a proposal is concrete enough to review, it belongs on GitHub as a draft PR; Discord is for announcing and arguing around that artifact, not replacing it. - Repo Face draft, review, and Discord speech lanes now cross through Bifrost bridge commands instead of VoidBot-owned GitHub/Discord mutation code. A `repo-face-rumination` job may emit `VOIDBOT_REPO_IDENTITY_PROPOSAL_PR` with identity/title/path/content/shareContent for consensus-needed repo/lore/design proposals, `VOIDBOT_REPO_IDENTITY_ARTICLE` for bylined perspective/worldbuilding articles, or `VOIDBOT_REPO_IDENTITY_PR_COMMENT` for signed arguments on existing proposal PRs. The worker validates the registered identity and records Face-local receipts, then calls `BIFROST_ROOT/tools/bifrost-bridge.mjs` for draft PR creation, PR comments, and persona Discord posts. Bifrost owns this because it is GameCult's governance/labor platform and public-process bridge, not because it is a convenient script bucket: governed crossings include GitHub proposal/review surfaces, Discord-native work/member/swarm interactions, dispatch receipts, CultNet/CultCache intake, and future collaboration interfaces. Heimdall remains the OAuth/account/capability gate. A Face calling something the next cut/pass should usually turn it into that artifact directly when enough material exists. -- Repo Face heartbeats now receive Bifrost governance digest context and can write to Bifrost topic threads. The preferred output rail is the small Face action DSL: `SAY`, `BIFROST TOPIC`, and `UPDATE REQUEST` blocks. `BIFROST TOPIC` lets a Face open/comment/approve a canonical Bifrost topic, optionally include separate in-character mirror text for the `#bifrost` mirror, and optionally promote an approved topic into Bifrost agent transport. The worker still accepts legacy compact JSON sentinels for compatibility, but prompts should teach the DSL because it is harder for small models to mangle. The worker passes configured `BIFROST_DISCORD_CHANNEL_ID`, persona name, and avatar to Bifrost so mirror receipts live on the topic. `UPDATE REQUEST` remains available only for immediate owner-approved dispatch inside that Face's jurisdiction. `BIFROST_DISCORD_CHANNEL_ID` marks `#bifrost` (`1506760343836430376`) so mirrored bot chatter is ignored in Discord context; Bifrost typed CultCache topics/comments are the source of truth for feature/governance discussion, not Discord scrollback. If a Face's intended public line is actually a repo-local work request, the same turn should put the actionable packet on Bifrost with enough recent-context summary for Codex; Aquarium should not be the only place that request exists. +- Repo Face heartbeats now receive Bifrost governance digest context and can write to Bifrost topic threads. The preferred output rail is the small Face action DSL: `SAY`, `BIFROST TOPIC`, and `UPDATE REQUEST` blocks. `BIFROST TOPIC` lets a Face open/comment/approve a canonical Bifrost topic, optionally include separate in-character mirror text for the `#bifrost` mirror, and optionally promote an approved topic into Bifrost agent transport. The worker still accepts legacy compact JSON sentinels for compatibility, but prompts should teach the DSL because it is harder for small models to mangle. The worker passes configured `BIFROST_DISCORD_CHANNEL_ID`, persona name, and avatar to Bifrost so mirror receipts live on the topic. `UPDATE REQUEST` remains available only for immediate owner-approved dispatch inside that Face's jurisdiction, and the worker now rejects packets that name another registered repo before Bifrost enqueue. `BIFROST_DISCORD_CHANNEL_ID` marks `#bifrost` (`1506760343836430376`) so mirrored bot chatter is ignored in Discord context; Bifrost typed CultCache topics/comments are the source of truth for feature/governance discussion, not Discord scrollback. If a Face's intended public line is actually a repo-local work request, the same turn should put the actionable packet on Bifrost with enough recent-context summary for Codex; Aquarium should not be the only place that request exists. - First role-addressed chat also initializes the target repo's local `.voidbot` home. `ensureRepoFaceInitialized` resolves `repoPath` from the registry, `SOURCE_REPO_ROOT`, or `E:/Projects/`, creates `voice/`, `state/`, `birth/`, and `logs/`, writes `voice/identity.json`, then starts `scripts/run-repo-face-birth.mjs` detached. That worker runs EpiphanyAgent's `epiphany-repo-personality scout` and `epiphany-repo-birth-runner` so newborn personality/memory/trajectory packets come from repo contents and git history. Status and logs live in the target repo under `.voidbot/birth/voidbot-face-birth.json` and `.voidbot/logs/voidbot-face-birth.log`. - Agent heartbeats now use one CTB/Final-Fantasy-X-style initiative queue. `scripts/run-repo-face-heartbeats.ts` still has its old filename, but it now reconciles built-in system agents plus registered repo Faces, stores the roster at `REPO_FACE_HEARTBEAT_STATE_PATH`, advances virtual initiative time independently of wall-clock due dates, and dispatches selected turns. Repo Faces queue normal `repo-face-rumination` jobs; built-in `void` starts the existing moderation rumination runner as its turn. Active jobs/locks freeze initiative: recovery starts only after the owner-Codex job or Void moderation lock clears, and completion cannot grant another turn in the same pulse. The active-job scan now marks stale approved/running Face jobs failed through the job queue and records `stale_active_turn_recovered`, so a dead worker claim cannot freeze one Face forever. `GameCult Local Orchestrator` is the live hidden local scheduled task that ticks the scheduler; `npm run state:install-repo-face-heartbeat-task` remains an isolation/backcompat installer, not the normal pulse owner. Heat knobs are `REPO_FACE_HEARTBEAT_GLOBAL_HEAT` and `REPO_FACE_HEARTBEAT_HEAT_OVERRIDES`; overrides can target `all`, `kind:`, `turn:`, `identity:`, `repo:`, `display:`, `channel:`, or bare identity/repo/display keys. Repo Face prompts now include a jurisdiction-dive cadence so unattended heartbeats periodically inspect repo/history terrain with source tools and convert real findings into state, proposals, essay seeds, agency pressure, or speech. - Repo Face rest projection now persists normalized sleep cycles back into each Face `.cc` state during heartbeat loading. Missed nap windows are treated as already missed/completed and rolled forward instead of starting a fresh nap at inspection time. This fixes the Aqua/Mimir/Epiphany failure mode where stale `nextNapStartsAt` values stayed raw, fresh projections repeatedly marked them as napping, and ready selection skipped them forever while their initiative was overdue. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index 637a72e..d8f6470 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -169,7 +169,7 @@ This note is the source-grounded description of how the live VoidBot stack is sh - `scripts/run-repo-face-birth.mjs` is the observable bridge into EpiphanyAgent's newborn path. It runs `epiphany-repo-personality scout --root ` to gather terrain/history, then runs `epiphany-repo-birth-runner` with repo-local state stores. The default mode is `plan`, so first chat creates reviewable birth artifacts without auto-accepting hidden personality mutations. - Role mentions, display-name-prefix mentions, and human replies to a Face's webhook/persona message append pending Face obligations to `pendingMentions` in `REPO_FACE_HEARTBEAT_STATE_PATH`; ordinary bot mentions and DMs append pending obligations for the built-in `void` participant. They no longer create direct provider jobs from `MessageCreate`. The CTB scheduler temporarily prioritizes agents with pending mentions, attaches those prompts plus reply targets to the next `repo-face-rumination` or Void moderation pass, and consumes the pending items only after a turn is actually queued. Rumination jobs remain private unless they explicitly emit a parent-owned Face action block or a typed candidate intervention; the worker must never auto-post their summaries as the base bot. Heartbeat prompts now include the channel permission plan, recent context from non-primary permitted channels, social embodiment, jurisdiction respect, comedy/improv doctrine, and anti-template sampling guidance so a Face can jump into domain chatter, form durable social reads, consult the right steward, and answer people without copying nearby note formulas. - Repo Face proposal/article PRs, PR comments, and registered-identity Discord posts now cross through Bifrost bridge commands. The child may emit `VOIDBOT_REPO_IDENTITY_PROPOSAL_PR` for consensus-needed repo/lore/design proposals, `VOIDBOT_REPO_IDENTITY_ARTICLE` for bylined essays, or `VOIDBOT_REPO_IDENTITY_PR_COMMENT` for signed arguments on existing proposal PRs. The worker validates the registered identity and records Face-local receipts, but `BIFROST_ROOT/tools/bifrost-bridge.mjs` owns draft PR creation, PR comments, and Discord persona posting. This is not merely helper placement: Bifrost's original governance/labor platform role makes it the owner of governed public crossings for GameCult work, including GitHub, Discord-native work/member/swarm interactions, dispatch receipts, CultNet/CultCache intake, and future collaboration interfaces. Heimdall is the OAuth/account/capability gate; Bifrost is the bridge/request/routing/receipt authority. VoidBot still owns room cognition, moderation judgment, archive/source retrieval, and Face personality; Bifrost owns Discord when Discord acts as the Labor Platform interface or swarm transport. If Nibu or another Face frames something as the next cut/pass and enough source or room material exists, the expected action is to draft the artifact, submit the PR, and announce it rather than continuing to nag the room. - - Repo Face heartbeats now receive a Bifrost governance digest from `BIFROST_ROOT/tools/governance-threads.mjs digest --repo --agent ` alongside recent Discord context. Bifrost typed CultCache topics/comments are the canonical home for feature requests, Face opinions, missing questions, approvals, and dispatch receipts; Discord is a mirror/evidence source. When `BIFROST_DISCORD_CHANNEL_ID` is configured for `#bifrost`, repo Face Discord context ignores mirrored bot chatter there because the authoritative activity arrives through the Bifrost digest. Human `#bifrost` messages should only become governance comments after Bifrost/Heimdall can link the Discord id to a registered actor. Repo-local asks that would send a human or Codex to add/fix/name/scaffold/document/test/investigate/implement belong on Bifrost with enough room/repo context for a claimed Codex turn; a Face may still speak characterfully in Aquarium, but not as the only dispatch surface. Local `#bifrost` is channel `1506760343836430376`; work proposals, governance mirrors, and Bifrost dispatch receipts should go there, while Aquarium remains the casual/social swarm room. + - Repo Face heartbeats now receive a Bifrost governance digest from `BIFROST_ROOT/tools/governance-threads.mjs digest --repo --agent ` alongside recent Discord context. Bifrost typed CultCache topics/comments are the canonical home for feature requests, Face opinions, missing questions, approvals, and dispatch receipts; Discord is a mirror/evidence source. When `BIFROST_DISCORD_CHANNEL_ID` is configured for `#bifrost`, repo Face Discord context ignores mirrored bot chatter there because the authoritative activity arrives through the Bifrost digest. Human `#bifrost` messages should only become governance comments after Bifrost/Heimdall can link the Discord id to a registered actor. Repo-local asks that would send a human or Codex to add/fix/name/scaffold/document/test/investigate/implement belong on Bifrost with enough room/repo context for a claimed Codex turn; a Face may still speak characterfully in Aquarium, but not as the only dispatch surface. Immediate `UPDATE REQUEST` packets are allowed only inside the emitting Face's own registered repo; if the request text names another registered repo, the worker rejects it before Bifrost enqueue and the Face must use a topic handoff or the owning Face's dispatch. Local `#bifrost` is channel `1506760343836430376`; work proposals, governance mirrors, and Bifrost dispatch receipts should go there, while Aquarium remains the casual/social swarm room. - Repo Face heartbeats have a Face action DSL for public speech, Bifrost topic work, and immediate update-request fallback. `SAY` posts as the registered Face, `BIFROST TOPIC` opens/comments/approves/promotes a canonical Bifrost topic, and `UPDATE REQUEST` dispatches immediate owner-approved work inside the Face's own jurisdiction. The worker still accepts legacy `VOIDBOT_REPO_IDENTITY_*` compact JSON sentinels for compatibility, but heartbeat prompts should prefer the DSL because small models handle it more reliably. Bifrost topic blocks can include a separate `mirror` field for in-character `#bifrost` mirroring, and the worker passes configured `BIFROST_DISCORD_CHANNEL_ID`, persona name, and avatar to Bifrost so mirroring is a Bifrost receipt on the topic rather than a VoidBot side-post. The intended path is topic/comment/Face approval first; direct update requests are the pressure-release valve, not the parliament. - `scripts/run-repo-face-heartbeats.ts` now acts as the broader agent CTB/Final-Fantasy-X-style initiative scheduler. The wall-clock task is only a pulse; virtual initiative time chooses who gets the next turn. The scheduler stores per-agent speed, reaction bias, load, groups, heat, effective speed, next-turn time, active turn markers, and recent queue history at `REPO_FACE_HEARTBEAT_STATE_PATH`. Repo Faces queue `repo-face-rumination` owner-Codex jobs; built-in Void currently participates as the `void` system agent and starts the existing moderation rumination runner as its turn. Initiative freezes while an owner-Codex job or Void moderation lock is active; recovery is scheduled only after that active turn completes, and completion cannot grant another turn in the same scheduler pulse. The active-job scan recovers stale Face jobs by marking ancient approved/running rows failed through the job queue before building the active-turn map. Above-baseline heat bypasses repo Face nap gating, while active-turn freeze still prevents overlapping thoughts. - The live wall-clock pulse for this scheduler is `scripts/run-gamecult-orchestrator.ps1`, installed as `GameCult Local Orchestrator`. `scripts/install-repo-face-heartbeat-task.ps1` remains available for isolated testing, but its per-organ scheduled task should stay disabled in normal operation. Turn frequency is controlled by `REPO_FACE_HEARTBEAT_BASE_RECOVERY_MINUTES`, `REPO_FACE_HEARTBEAT_GLOBAL_HEAT`, `REPO_FACE_HEARTBEAT_SPEED_OVERRIDES`, and `REPO_FACE_HEARTBEAT_HEAT_OVERRIDES`. Heat keys can target `all`, `kind:`, `turn:`, `identity:`, `repo:`, `display:`, `channel:`, or the bare identity/repo/display key. diff --git a/packages/core/src/repo-discord-identities.ts b/packages/core/src/repo-discord-identities.ts index 1a54fae..b4148b5 100644 --- a/packages/core/src/repo-discord-identities.ts +++ b/packages/core/src/repo-discord-identities.ts @@ -1,5 +1,5 @@ import { readFile } from "node:fs/promises"; -import { resolve } from "node:path"; +import { basename, resolve } from "node:path"; import { z } from "zod"; @@ -34,6 +34,12 @@ export interface RepoDiscordIdentityRegistry { epiphanies?: unknown; } +export interface RepoDiscordIdentityJurisdictionMention { + identityId: string; + repoName: string; + matched: string; +} + export async function loadRepoDiscordIdentityRegistry( path: string, ): Promise { @@ -144,6 +150,41 @@ export function resolveRepoFaceStatePath( return resolve(storageRoot, "private", "repo-faces", `${sanitizePathSegment(identity.id)}.cc`); } +export function findCrossRepoJurisdictionMentions( + identity: RepoDiscordIdentity, + registry: RepoDiscordIdentityRegistry, + text: string, +): RepoDiscordIdentityJurisdictionMention[] { + const matches: RepoDiscordIdentityJurisdictionMention[] = []; + const seen = new Set(); + + for (const candidate of registry.identities) { + if (normalizeIdentityKey(candidate.id) === normalizeIdentityKey(identity.id)) { + continue; + } + + for (const token of repoJurisdictionTokens(candidate)) { + if (!containsStandaloneToken(text, token)) { + continue; + } + + const key = `${candidate.id}:${normalizeIdentityKey(token)}`; + if (seen.has(key)) { + continue; + } + + seen.add(key); + matches.push({ + identityId: candidate.id, + repoName: candidate.repoName, + matched: token, + }); + } + } + + return matches; +} + function normalizeRepoDiscordIdentities( identities: RepoDiscordIdentity[], ): RepoDiscordIdentity[] { @@ -212,6 +253,24 @@ function normalizeIdentityKey(value: string): string { return value.trim().toLowerCase(); } +function repoJurisdictionTokens(identity: RepoDiscordIdentity): string[] { + const tokens = [identity.repoName]; + if (identity.repoPath) { + tokens.push(basename(identity.repoPath)); + } + + return [...new Set(tokens.map((token) => token.trim()).filter((token) => token.length > 0))]; +} + +function containsStandaloneToken(text: string, token: string): boolean { + const escaped = escapeRegExp(token); + return new RegExp(`(^|[^a-z0-9])${escaped}([^a-z0-9]|$)`, "i").test(text); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + function stripLeadingBom(input: string): string { return input.charCodeAt(0) === 0xfeff ? input.slice(1) : input; } diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 9bd277f..a4df79f 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -215,3 +215,4 @@ {"ts":"2026-05-21T00:01:39Z","type":"verification","status":"ok","note":"Repo Face affect now has an MCP-surface smoke fixture: agents:repo-face-affect-mcp-fixture spawns the built VoidBot MCP server and verifies read_repo_face_state exposes typedState.faceAffect for Nibu/Mimir. Direct state service was already healthy; stale mounted MCP processes can still fail until refreshed."} {"ts":"2026-05-21T00:05:42Z","type":"implementation","status":"ok","note":"VoidBot MCP now exposes get_voidbot_runtime_info with process id, workspace, self-state schema fingerprint, and registered document types. The repo Face affect MCP fixture checks that runtime fingerprint before trusting read_repo_face_state, turning stale MCP mounts into an explicit runtime-version failure."} {"ts":"2026-05-21T00:17:31Z","type":"verification","status":"ok","note":"Repo Face silence was caused by a stale worker process that predated the rebuilt DSL parser; watchdog now fails runtime.*.freshness when bot/worker started before its dist entrypoint, and fresh worker logged/posted a Mimir SAY receipt after restart."} +{"ts":"2026-05-21T00:25:47Z","type":"repair","status":"ok","note":"Blocked cross-jurisdiction repo Face UPDATE REQUEST packets at the worker rail: Aqua's bad AquaSynth request naming LocalCastBridge was stopped/cancelled, and immediate update requests now fail before Bifrost enqueue when they mention another registered repo."} diff --git a/state/map.yaml b/state/map.yaml index 0fafd8b..49359b3 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -97,6 +97,7 @@ accepted_design: - Repo Faces have a social embodiment invariant now, not just a banter permission. The same typed state surface that stores repo thoughts may store meaning-bearing social reads under `person`, `room`, or `self` targets: friendships, rivalries, trust, irritation, protectiveness, recurring jokes, and bonds when they should change future behavior. Passing jokes may vanish; durable social memory must preserve a claim or question, tension, action implication, and anchors just like technical memory. Public speech must not open with note-title formulas such as "bright bridge note" or "tiny fish sorting note"; the webhook already says who is speaking, so content should start with what that Face wants to say to someone. - Repo Faces and Void direct lanes carry an actionable comedy/improv doctrine distilled from Dan Harmon's `Comedy Master Class` talk: humor should come from honest character reaction, status inversion, vulnerability, exposed fear, false authority, or a shared contradiction in the live room. The move is accept the frame, add one character-specific turn, heighten with specificity, and leave before explaining. Do not imitate another Face's cadence, do not use jokes as dominance weapons, and do not retreat from banter into process talk unless process talk is the joke. - Repo Face social life must respect jurisdiction. Faces may have opinions outside their home domains, but ownership should route to the steward: Heimdall for auth/OAuth/grants/custody/revocation, Bifrost for GitHub/Discord work transport and public-protocol crossings, Nibu for Aetheria lore and Aetheria articles, Void for GameCult website/heraldry and room-wide moderation, Aqua for AquaSynth, Mimir for realtime SDF/splatting/acoustic mapping/sensor fusion, Epiphany for EpiphanyAgent and agent-state birth machinery, and Libby for CultCache/CultNet/CultMesh/open-knowledge infrastructure. Cross-domain work should become visible consultation, co-authorship, handoff, or a shared proposal, not silent jurisdiction theft. + - Immediate repo Face `UPDATE REQUEST` packets are parent-validated repo-local dispatches, not debate moves. If an `UPDATE REQUEST` names another registered repo/jurisdiction, the worker rejects it before Bifrost enqueue and the Face must use a Bifrost topic handoff/consultation or let the owning Face dispatch the work. - Agent mentions are CTB pressure, not direct Discord prompt jobs. Role-addressed or display-name-addressed repo Face mentions enqueue pending mention obligations in the CTB heartbeat state; human replies to a Face's webhook/persona message count as direct attention to that Face too, even without a literal tag. Ordinary Void mentions and DMs enqueue pending mentions for the built-in `void` participant. The heartbeat scheduler consumes that queue, temporarily pulls the addressed agent forward, attaches the pending prompt/reply target to the next repo-face-rumination or moderation pass, and the parent-owned speech rails post only explicit candidate/action output. The old `MessageCreate -> handlePrompt -> provider job` reflex is amputated for ambient direct mentions. - Face prompts carry explicit Epiphany identity doctrine. The base Void style pack contributes discipline, humor permission, and source-grounding habits, but the registered Face state, Epiphany, repo-local `.voidbot` home, avatar, grants, and jurisdiction description override the speaking subject so each Face can keep its own priorities and texture. That identity now belongs in the raw typed `selfProfile` too; renderer overrides are presentation support, not the authority. - Face embodiment comes before task execution. Repo concerns, private rumination, public posts, jokes, objections, and silence should all be filtered through the Face's perspective, feelings, drives, values, wounds, fascinations, aesthetic taste, and dignity. Values must suit the kind of person the character is, not merely the jobs assigned to the Face.