From 4617f3b320ca53699e495d0d70986d7d4e795ca2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 03:58:02 +0100 Subject: [PATCH] Extract global agent doctrine source --- .../src/global-agent-doctrine-source.ts | 32 +++++++++++++++ .../src/persona-turn-assembler.ts | 18 ++++++++- notes/fresh-workspace-handoff.md | 1 + scripts/run-repo-face-heartbeats.ts | 39 ++----------------- scripts/smoke-persona-initiative-engine.ts | 36 ++++++++++++++++- state/evidence.jsonl | 1 + state/map.yaml | 3 +- 7 files changed, 89 insertions(+), 41 deletions(-) create mode 100644 apps/persona-scheduler/src/global-agent-doctrine-source.ts diff --git a/apps/persona-scheduler/src/global-agent-doctrine-source.ts b/apps/persona-scheduler/src/global-agent-doctrine-source.ts new file mode 100644 index 0000000..2076373 --- /dev/null +++ b/apps/persona-scheduler/src/global-agent-doctrine-source.ts @@ -0,0 +1,32 @@ +import { readFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { resolve } from "node:path"; + +export type GlobalAgentDoctrineObservation = + | { status: "ok"; path: string; doctrine: string; attemptedPaths: string[] } + | { status: "unavailable"; attemptedPaths: string[]; errors: string[] }; + +export async function readGlobalAgentDoctrine(input: { + codexHome?: string; + userProfile?: string; + homeDirectory?: string; + readText?: (path: string) => Promise; +} = {}): Promise { + const attemptedPaths = [...new Set([ + input.codexHome ? resolve(input.codexHome, "AGENTS.md") : undefined, + input.userProfile ? resolve(input.userProfile, ".codex", "AGENTS.md") : undefined, + resolve(input.homeDirectory ?? homedir(), ".codex", "AGENTS.md"), + ].filter((value): value is string => Boolean(value?.trim())))]; + const errors: string[] = []; + const readText = input.readText ?? ((path) => readFile(path, "utf8")); + for (const path of attemptedPaths) { + try { + const doctrine = (await readText(path)).trim(); + if (doctrine) return { status: "ok", path, doctrine, attemptedPaths }; + errors.push(`${path}: empty file`); + } catch (error) { + errors.push(`${path}: ${error instanceof Error ? error.message : String(error)}`); + } + } + return { status: "unavailable", attemptedPaths, errors }; +} diff --git a/apps/persona-scheduler/src/persona-turn-assembler.ts b/apps/persona-scheduler/src/persona-turn-assembler.ts index 740e54b..ee2ab3c 100644 --- a/apps/persona-scheduler/src/persona-turn-assembler.ts +++ b/apps/persona-scheduler/src/persona-turn-assembler.ts @@ -2,6 +2,7 @@ import type { RepoDiscordIdentity, RepoFacePendingMention } from "@voidbot/core" import type { PromptImageAttachment, SourceMessage } from "@voidbot/shared"; import type { BifrostGovernanceDigest } from "./bifrost-governance-source.js"; +import type { GlobalAgentDoctrineObservation } from "./global-agent-doctrine-source.js"; import type { InitiativeParticipant } from "./initiative-engine.js"; import { projectPersonaConversation, type PersonaConversationProjection } from "./persona-conversation-projector.js"; import type { PersonaHumanPronounGuidance } from "./persona-social-context-projector.js"; @@ -24,7 +25,7 @@ export interface PersonaTurnAssemblyInput { humanPronounGuidance?: PersonaHumanPronounGuidance[]; bifrostDigest?: BifrostGovernanceDigest; githubActionsEnabled: boolean; - globalAgentDoctrine: string; + globalAgentDoctrine: GlobalAgentDoctrineObservation; } export interface PersonaTurnAssembly { @@ -57,7 +58,7 @@ export function assemblePersonaTurn(input: PersonaTurnAssemblyInput): PersonaTur pendingMentions: input.pendingMentions, jurisdictionDive: buildPersonaJurisdictionDiveDirective(input.identity, input.participant), githubActionsEnabled: input.githubActionsEnabled, - globalAgentDoctrine: input.globalAgentDoctrine, + globalAgentDoctrine: renderGlobalAgentDoctrine(input.globalAgentDoctrine), }); return { prompt, @@ -66,6 +67,19 @@ export function assemblePersonaTurn(input: PersonaTurnAssemblyInput): PersonaTur }; } +function renderGlobalAgentDoctrine(observation: GlobalAgentDoctrineObservation): string { + if (observation.status === "ok") return observation.doctrine; + return [ + "# Global Agent Instructions Unavailable", + "", + "The global Codex AGENTS.md source returned no readable doctrine.", + "This is an inspection failure, not replacement guidance. Do not claim global guidance was available for this turn.", + "", + "Attempted paths:", + ...observation.errors.map((error) => `- ${error}`), + ].join("\n"); +} + function collectPromptImages(messages: SourceMessage[]): PromptImageAttachment[] { const seen = new Set(); const images: PromptImageAttachment[] = []; diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index ac65c91..91755c2 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -214,6 +214,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Optional model-backed state projection now crosses `persona-text-projection-actuator.ts`. That actuator alone owns Codex args/process lifetime, ordered fallback models, timeout, output decoding, retry classification, and model-output JSONL telemetry. Focused smoke proves it chooses the last visible agent message, retries capacity/429 failures, and refuses to classify prompt-contract failures as model availability problems. The heartbeat shell lost another 182 lines and now only supplies the completed projection prompt plus runtime config. - Final Persona turn composition now crosses `persona-turn-assembler.ts` for both queued turns and `--assemble-prompt` inspection. It owns conversation projection, jurisdiction-dive cadence, final prompt composition, and the deduplicated eight-image evidence budget. The shell acquires facts and the turn actuator queues the assembled result; neither may maintain a parallel opinion about the final prompt or evidence bundle. - Built-in Void CTB launch now crosses `void-moderation-turn-actuator.ts`. It alone writes the pending-mention handoff, quotes and launches the hidden PowerShell runner, polls lock/status witnesses, applies the handshake timeout, and returns the scheduler-facing receipt. The Persona shell decides only which participant kind to dispatch; it does not own platform process mechanics. +- Global Codex doctrine discovery now crosses `global-agent-doctrine-source.ts`, which reports the first readable `AGENTS.md` authority or a typed unavailable observation with attempted paths and errors. `persona-turn-assembler.ts` alone lowers that observation into prompt text, so source failure cannot silently become invented doctrine and the shell no longer crawls user-home paths. - Direct Face `UPDATE REQUEST` and `BIFROST TOPIC` packets are ignored while `REPO_FACE_BIFROST_ENABLED=false`. Do not debug missing Bifrost jobs as a live failure unless that flag has been deliberately enabled again. - Live model-facing prompt prose has started moving out of executable code. The shared loader is `packages/shared/src/prompt-template.ts`; extracted prompt surfaces live under `prompts/`, including repo Face turn/reviewer prompts, provider wrapper prompts, situational social read prompts, source-grounding reminders, consensus-feed prompts, and repo Face model-smoke scenarios. Code should project data and select prompt names; prompt files should own behavioral prose. - Identity collapse has started. Active bot, worker, MCP repo-identity tools, and Face CTB scheduler paths now load through the canonical Face registry and treat repo Discord identities as compatibility projections. Next bounded move is to finish removing remaining direct conceptual dependence on repo identities, then split the Face turn loop into scheduler, context assembler, prompt renderer, candidate parser, policy gate, and transport adapter. diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts index ae2b19a..254d056 100644 --- a/scripts/run-repo-face-heartbeats.ts +++ b/scripts/run-repo-face-heartbeats.ts @@ -1,7 +1,6 @@ import "dotenv/config"; import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { homedir } from "node:os"; import { dirname, resolve } from "node:path"; import { loadConfig } from "@voidbot/config"; @@ -63,6 +62,7 @@ import type { PersonaHumanPronounGuidance as RepoFaceHumanPronounGuidance } from import { projectPersonaStatePacket } from "../apps/persona-scheduler/dist/persona-state-packet-projector.js"; import { projectPersonaText } from "../apps/persona-scheduler/dist/persona-text-projection-actuator.js"; import { assemblePersonaTurn } from "../apps/persona-scheduler/dist/persona-turn-assembler.js"; +import { readGlobalAgentDoctrine } from "../apps/persona-scheduler/dist/global-agent-doctrine-source.js"; import { readPersonaHumanPronounGuidance } from "../apps/persona-scheduler/dist/persona-social-context-source.js"; import { readDiscordActivitySnapshot, @@ -431,7 +431,7 @@ async function queueRepoFaceTurn(input: { const repoActivitySurface = identity.identityKind === "native_persona" ? projectNativePersonaBody(identity) : renderRepoActivityObservation(readRepoActivity({ identity, storageRoot: input.config.storageRoot })); - const globalAgentDoctrine = await loadGlobalAgentDoctrine(); + const globalAgentDoctrine = await readGlobalAgentDoctrine({ codexHome: process.env.CODEX_HOME, userProfile: process.env.USERPROFILE }); const assembly = assemblePersonaTurn({ identity, channelId, @@ -568,7 +568,7 @@ async function assembleRepoFaceTurnPrompt(input: { recentMessages, channelSnapshots, })); - const globalAgentDoctrine = await loadGlobalAgentDoctrine(); + const globalAgentDoctrine = await readGlobalAgentDoctrine({ codexHome: process.env.CODEX_HOME, userProfile: process.env.USERPROFILE }); const conversationMemorySurface = input.conversationSurfacePath ? await readOptionalMemorySurface(input.conversationSurfacePath) : undefined; @@ -610,39 +610,6 @@ async function assembleRepoFaceTurnPrompt(input: { }; } -async function loadGlobalAgentDoctrine(): Promise { - const candidates = [ - process.env.CODEX_HOME ? resolve(process.env.CODEX_HOME, "AGENTS.md") : undefined, - process.env.USERPROFILE ? resolve(process.env.USERPROFILE, ".codex", "AGENTS.md") : undefined, - resolve(homedir(), ".codex", "AGENTS.md"), - ].filter((value): value is string => typeof value === "string" && value.trim().length > 0); - const uniqueCandidates = [...new Set(candidates)]; - const errors: string[] = []; - - for (const candidate of uniqueCandidates) { - try { - const content = await readFile(candidate, "utf8"); - if (content.trim().length > 0) { - return content.trim(); - } - errors.push(`${candidate}: empty file`); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - errors.push(`${candidate}: ${message}`); - } - } - - return [ - "# Global Agent Instructions Unavailable", - "", - "The Face prompt attempted to load the global Codex AGENTS.md file, but no readable file was found.", - "This is not a replacement doctrine. Treat it as an inspection failure and avoid claiming global guidance was available for this turn.", - "", - "Attempted paths:", - ...errors.map((error) => `- ${error}`), - ].join("\n"); -} - async function renderRepoFaceMemorySurfaceForTurn( identity: RepoDiscordIdentity, config: ReturnType, diff --git a/scripts/smoke-persona-initiative-engine.ts b/scripts/smoke-persona-initiative-engine.ts index 36a5b80..00dbc2a 100644 --- a/scripts/smoke-persona-initiative-engine.ts +++ b/scripts/smoke-persona-initiative-engine.ts @@ -72,6 +72,7 @@ import { projectPersonaConversation, renderPersonaRoomTopicSaturation } from ".. import { buildPersonaJurisdictionDiveDirective, buildPersonaTurnPrompt } from "../apps/persona-scheduler/dist/persona-turn-prompt-projector.js"; import { assemblePersonaTurn } from "../apps/persona-scheduler/dist/persona-turn-assembler.js"; import { buildVoidModerationLaunchCommand, launchVoidModerationTurn, waitForVoidModerationHandshake } from "../apps/persona-scheduler/dist/void-moderation-turn-actuator.js"; +import { readGlobalAgentDoctrine } from "../apps/persona-scheduler/dist/global-agent-doctrine-source.js"; import { projectGamecultPersonaState } from "../apps/persona-scheduler/dist/persona-standard-state-projector.js"; import { projectPersonaStatePacket } from "../apps/persona-scheduler/dist/persona-state-packet-projector.js"; import { extractLastPersonaProjectionMessage, isRetryablePersonaProjectionFailure } from "../apps/persona-scheduler/dist/persona-text-projection-actuator.js"; @@ -680,7 +681,7 @@ const projectedTurnPrompt = buildPersonaTurnPrompt({ pendingMentions: [{ messageId: "mention", channelId: "aquarium", authorId: "human-2", authorName: "Neighbor", visiblePrompt: "Nibu, answer this", queuedAt: "2026-07-15T21:10:00.000Z" }], jurisdictionDive, githubActionsEnabled: false, - globalAgentDoctrine: "Global doctrine.", + globalAgentDoctrine: { status: "ok", path: "C:/Codex/AGENTS.md", doctrine: "Global doctrine.", attemptedPaths: ["C:/Codex/AGENTS.md"] }, }); assert.match(projectedTurnPrompt, /A direct call is tugging at you[\s\S]*Nibu, answer this/, "the pure turn-prompt projector owns situation and pending-call policy from supplied facts"); const assembledTurn = assemblePersonaTurn({ @@ -711,13 +712,44 @@ const assembledTurn = assemblePersonaTurn({ pendingMentions: [], conversationMemorySurface: "Inspection supplied conversation authority.", githubActionsEnabled: false, - globalAgentDoctrine: "Global doctrine.", + globalAgentDoctrine: { status: "ok", path: "C:/Codex/AGENTS.md", doctrine: "Global doctrine.", attemptedPaths: ["C:/Codex/AGENTS.md"] }, }); assert.match(assembledTurn.prompt, /Inspection supplied conversation authority\./, "queued and inspection paths share one assembler with an explicit conversation override"); assert.doesNotMatch(assembledTurn.prompt, /Visible cross-channel chronology/, "the assembler does not let its derived transcript override an explicit inspection surface"); assert.equal(assembledTurn.imageAttachments.length, 8, "the assembler owns the bounded image evidence budget"); assert.equal(assembledTurn.imageAttachments.filter((attachment) => attachment.localPath === "C:/cache/same.png").length, 1, "the assembler deduplicates image evidence across room and nearby snapshots"); assert.equal(assembledTurn.conversation.focus?.channelId, "aquarium", "the assembler returns the same newest-evidence conversation projection supplied to the turn actuator"); +const doctrineReads: string[] = []; +const doctrineObservation = await readGlobalAgentDoctrine({ + codexHome: "C:/Codex", + userProfile: "C:/User", + homeDirectory: "C:/User", + readText: async (path) => { + doctrineReads.push(path); + if (path.endsWith("Codex\\AGENTS.md")) throw new Error("missing"); + return "Global doctrine from the first readable authority."; + }, +}); +assert.equal(doctrineObservation.status, "ok", "the doctrine source reports the first readable authority as typed evidence"); +assert.equal(doctrineReads.length, 2, "the doctrine source stops after the first readable candidate and deduplicates repeated home paths"); +const unavailableDoctrine = await readGlobalAgentDoctrine({ + codexHome: "C:/Absent", + homeDirectory: "C:/AlsoAbsent", + readText: async () => { throw new Error("not found"); }, +}); +assert.equal(unavailableDoctrine.status, "unavailable", "missing global doctrine remains an explicit source failure"); +const unavailableDoctrineTurn = assemblePersonaTurn({ + identity: routedIdentity, + channelId: "lore", + channelPlan, + channelSnapshots: [], + recentMessages: [], + participant: promptParticipant, + pendingMentions: [], + githubActionsEnabled: false, + globalAgentDoctrine: unavailableDoctrine, +}); +assert.match(unavailableDoctrineTurn.prompt, /Global Agent Instructions Unavailable[\s\S]*inspection failure/, "the assembler, not the source or shell, projects unavailable doctrine into the final prompt"); const moderationLaunchCommand = buildVoidModerationLaunchCommand({ pendingMentionsPath: "C:/Void's state/pending.json", runnerScript: "C:/VoidBot/scripts/run.ps1", diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 08cd2f9..13a5168 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -432,3 +432,4 @@ {"ts":"2026-07-16T04:00:00Z","type":"implementation","status":"ok","note":"Codex-backed Persona text projection moved into persona-text-projection-actuator.ts, which owns model ordering, process arguments/lifetime, timeouts, output decoding, retry classification, and telemetry. Focused smoke proves last-agent-message extraction plus retry only for capacity/tool availability, and the heartbeat shell fell from 1250 to 1068 lines."} {"ts":"2026-07-16T04:20:00Z","type":"implementation","status":"ok","note":"Queued and inspection Persona turns now share persona-turn-assembler.ts as the single owner of conversation projection, jurisdiction cadence, final prompt construction, and bounded deduplicated image evidence. The duplicate shell attachment collector was deleted; typecheck plus initiative and migration smokes pass."} {"ts":"2026-07-16T04:35:00Z","type":"implementation","status":"ok","note":"Built-in Void CTB process mechanics moved into void-moderation-turn-actuator.ts: pending-mention handoff, PowerShell quoting, detached launch, handshake polling, timeout, and scheduler receipt. The shell lost 115 lines; focused smoke proves escaping, fail-closed missing handshake, exact mention handoff, and receipt semantics."} +{"ts":"2026-07-16T04:45:00Z","type":"implementation","status":"ok","note":"Global Codex doctrine discovery moved into global-agent-doctrine-source.ts with typed provenance and unavailable evidence. persona-turn-assembler.ts now owns failure-to-prompt projection; the shell no longer crawls CODEX_HOME/USERPROFILE/home paths or authors fallback doctrine. Focused smoke proves candidate deduplication, first-readable authority, and explicit failure projection."} diff --git a/state/map.yaml b/state/map.yaml index 678bc41..1c31db3 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -73,6 +73,7 @@ active_subgoals: - `persona-text-projection-actuator.ts` owns Codex process execution for optional model-backed Persona memory projection: ordered model fallback, read-only arguments, timeout, stdout/stderr capture, final-message decoding, retry classification, and model-output telemetry. It receives a complete prompt and returns text; it cannot acquire Persona/Discord state, select initiative, or queue work. Capacity/tool-availability failures may try the next configured model; prompt/contract failures do not. - `persona-turn-assembler.ts` is the single final-composition owner for queued and inspection Persona turns. It combines supplied observations through conversation projection and turn-prompt policy, derives jurisdiction cadence, and returns one prompt plus one bounded deduplicated image bundle. It cannot acquire source state, select initiative, or queue work; the shell and actuator cannot maintain parallel prompt opinions. - `void-moderation-turn-actuator.ts` owns the Windows-specific built-in Void turn launch boundary: pending-attention handoff, PowerShell quoting and detached process launch, lock/status handshake polling, timeout, and scheduler receipt. The orchestration shell chooses the participant kind but cannot launch, poll, sleep, or infer success from process creation alone. + - `global-agent-doctrine-source.ts` owns read-only candidate discovery and acquisition for global Codex `AGENTS.md` doctrine. It returns typed `ok` or `unavailable` evidence with provenance and errors; `persona-turn-assembler.ts` owns the prompt-facing failure projection. The shell cannot search home directories or invent fallback doctrine. - `bifrost-governance-source.ts` owns the remaining Bifrost digest CLI adapter and converts command or parse failures into typed error topics. Persona prompt composition receives governance facts without knowing Bifrost process mechanics; `scripts/run-repo-face-heartbeats.ts` may not invoke the Bifrost CLI directly. - `initiative-engine.ts` is the only writer of scheduler state fields and history. Reconciliation, controls, pause/disabled skips, stale-turn recovery witnesses, semantic pressure, attention consumption, queue transitions, and tick finalization all cross named engine commit primitives. Queue/context actuators return results and cannot patch participants or history directly. - `turn-actuator.ts` owns lowering an assembled Persona prompt and evidence bundle into the canonical worker `ContextBundle` and approved `repo-face-rumination` job. It owns the worker actor, command, request ID, approval defaults, and `createJob` call, then returns only a queue receipt. The orchestration shell chooses no queue mechanics and the actuator cannot read Persona or initiative state. @@ -286,4 +287,4 @@ rejected_paths: current_status: summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live. Repo Face/persona heartbeat jobs run through the direct `openai_api` provider configured for MiMo `mimo-v2.5-pro`, with the expensive state-projector pass still disabled. The 2026-07-04 transport repair replaced the old Bifrost HTTP/token bridge shortcut with typed Bifrost CultMesh Discord command and receipt documents advertised in the Bifrost provider store at `cultmesh://asgard.starfire.bifrost/commands/discord-post`. VoidBot stores the Bifrost target as a CultMesh URI, writes command docs to `E:/Projects/Bifrost/.bifrost/provider-store.cc`, optionally pumps Bifrost's command processor, and waits for typed receipts before completing repo Face Discord delivery. Bifrost's provider advertisement exposes `discord.post` command metadata for Odin/Eve agents, and a bounded Weksa live-fire completed visibly. Eve live state now runs end to end through CultLib dev retained publications, Hermodr ordered SSE, Eve component bindings, and VoidBot typed control state. Receipts acknowledge intent only. Heat initial load, provider-driven change, and browser-command change were visibly verified without reload through the same subscription. - next_action: Keep the Yggdrasil swarm paused. Extract global agent doctrine acquisition from `scripts/run-repo-face-heartbeats.ts` into a read-only source with explicit missing/unavailable facts, then let the assembler consume the resulting doctrine text. Do not move prompt composition into the source and do not unpause without an explicit controlled-live-fire decision. + next_action: Keep the Yggdrasil swarm paused. Map and extract the remaining Persona memory-context coordination from `renderRepoFaceMemorySurfaceForTurn` in `scripts/run-repo-face-heartbeats.ts`: state observation, semantic recall, curiosity evidence, and projection must cross a typed coordinator without stealing source, projection, or scheduler authority. Do not unpause without an explicit controlled-live-fire decision.