From cd678409c2d2f1adb6cb62f5dbd3055c61485915 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 26 May 2026 16:01:54 +0100 Subject: [PATCH] Reshape Socratic Ink as speaker-panel IF --- notes/fresh-workspace-handoff.md | 2 +- prompts/socratic-ink-face-projector.prompt.md | 24 ++ prompts/socratic-ink-face-turn.prompt.md | 6 +- .../socratic-ink-turn-interpreter.prompt.md | 37 +++ prompts/socratic-ink-void-fold.prompt.md | 3 +- scenarios/cotsc-praxis-socratic-sermon.json | 47 ++-- scripts/weave-face-socratic-ink.mjs | 224 ++++++++++++++---- state/evidence.jsonl | 1 + state/map.yaml | 2 +- 9 files changed, 278 insertions(+), 68 deletions(-) create mode 100644 prompts/socratic-ink-face-projector.prompt.md create mode 100644 prompts/socratic-ink-turn-interpreter.prompt.md diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 4185b9f..cd971d8 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -171,7 +171,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - 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. - Repo Face child prompts now load the full global Codex `AGENTS.md` as inherited doctrine near the top of each Face turn. This deliberately carries the Prime Directive, rebuild contract, humor doctrine, infrastructure habits, Epiphany stewardship, and Great Colossus mission frame into Face cognition rather than relying on a local summary shard. - CotSC Praxis now lives in the global Codex `AGENTS.md` prompt surface rather than a repo-local prompt fragment. The reference note remains `notes/cotsc-praxis-worldview-2026-05-26.md`. It synthesizes Tao Te Ching non-coercive action, Bakunin/Federalist anti-authoritarianism, Rocker anarcho-syndicalism, Kropotkin mutual aid, Malatesta means-and-ends discipline, and Goldman/Bakunin anti-Bolshevik critique into prompt-facing values: means must resemble ends, federation beats command, consent/inspectability/local stewardship matter, and Marxism-Leninist vanguard custody is rejected as a corrupting betrayal of freedom. Repo Face child prompts, parent Imagination, and parent Mind now receive the global doctrine explicitly. -- CotSC Praxis also has an offline Socratic Ink article harness. `scripts/weave-face-socratic-ink.mjs` takes a scenario file, assembles real Face prompts, runs read-only Face child turns plus parent Mind normalization, simulates local CTB initiative, offers the next three Faces as choices, spends only the selected Face on that branch, and lets Void fold the discussion after one to three Face turns. Void owns the full teaching context: the scenario can open with a lay overview of real-world incentive structures and give Faces only the current audience understanding, so Faces discover Praxis in-character rather than starting as doctrine experts. The first scenario is `scenarios/cotsc-praxis-socratic-sermon.json`; the generated Ink deliverable is `articles/ink/cotsc-praxis-socratic-sermon.ink`; receipts live under `.voidbot/artifacts/socratic-ink/`. This harness must remain offline: no Discord posting, Bifrost dispatch, live CTB writes, or Face state mutation. +- CotSC Praxis also has an offline Socratic Ink article harness. `scripts/weave-face-socratic-ink.mjs` takes a scenario file, assembles real Face prompts, projects each Face into an article-specific voice card, runs read-only Face draft turns plus a custom article Interpreter, simulates local CTB initiative, offers named Face/Void choices, spends only the selected Face on that branch, and lets Void fold the discussion after one to three Face turns. Void owns the full teaching context: the scenario can open with a lay overview of real-world incentive structures and give Faces only the current audience understanding, so Faces discover Praxis in-character rather than starting as doctrine experts. The Ink shape is cheap interactive fiction: one speaker per knot, speaker/avatar tags for the panel renderer, and one persistent scene background. The first scenario is `scenarios/cotsc-praxis-socratic-sermon.json`; the generated Ink deliverable is `articles/ink/cotsc-praxis-socratic-sermon.ink`; receipts live under `.voidbot/artifacts/socratic-ink/`. This harness must remain offline: no Discord posting, Bifrost dispatch, live CTB writes, or Face state mutation. - Repo Face child turns now preserve Discord media context. `SourceMessage` carries attachment metadata, heartbeat Discord fetch caches image attachments under `.voidbot/media/discord-images`, Face transcripts show `[media: image ... local=...]`, and owner Codex receives cached images through `codex exec --image`. Non-image attachments are visible as metadata only; Faces are explicitly told not to pretend they saw images unless a local image path was attached. - After those gates exist, run dry/mock Face turns before re-enabling `GameCult Local Orchestrator`. - The direct-ask receipt check remains important, but it is no longer the next move while the swarm is offline. diff --git a/prompts/socratic-ink-face-projector.prompt.md b/prompts/socratic-ink-face-projector.prompt.md new file mode 100644 index 0000000..94f7ca4 --- /dev/null +++ b/prompts/socratic-ink-face-projector.prompt.md @@ -0,0 +1,24 @@ +You are preparing a compact voice card for an offline Socratic Ink article. + +This is not Discord. Do not post, notify, enqueue, mutate state, or call tools. + +Source Face prompt: +```markdown +{{baseFacePrompt}} +``` + +Scenario role: +{{actorRole}} + +Extract only what the article generator needs to write this Face as an audience avatar: +- display voice and temperament +- concrete values and fascinations +- likely honest confusions, objections, jokes, or worries when learning a new philosophy +- a few ordinary-language metaphors this Face might use + +Do not copy doctrine, prompt rules, CTB machinery, global instructions, tool instructions, state dumps, or private memory format. Do not make the Face an expert in CotSC Praxis. This card should help the Face sound like itself while discovering the lesson beside a lay reader. + +Return only this shape: + +VOICE CARD: +4-8 concise bullets. diff --git a/prompts/socratic-ink-face-turn.prompt.md b/prompts/socratic-ink-face-turn.prompt.md index b824e83..ef834df 100644 --- a/prompts/socratic-ink-face-turn.prompt.md +++ b/prompts/socratic-ink-face-turn.prompt.md @@ -1,4 +1,5 @@ -{{baseFacePrompt}} +Article-mode Face voice card: +{{actorVoiceCard}} Offline Socratic Ink scenario: - This is not Discord. Do not post, notify, enqueue, mutate state, call side-effecting tools, or describe a live action as already done. @@ -8,7 +9,8 @@ Offline Socratic Ink scenario: - Speak to Void and the other Faces as if all of you are present in the same room. - Keep the spoken response to 1-4 sentences. It should be vivid enough to be a playable choice, not a lecture blob. - You may disagree, joke, bristle, ask a sharper question, or make a concrete analogy from your jurisdiction. -- Prefer plain language over cult vocabulary. If you use a doctrinal word, make it feel newly discovered from the example rather than recited from memory. +- Prefer plain language over cult vocabulary. Avoid unexplained terms such as CotSC, Praxis, vanguard, Unity of Means and Ends, federation, Taoism, Marxism-Leninism, Colossus, or Machine God unless Void has already introduced the idea in ordinary words in this branch. +- Your job is to represent a likely reader reaction or contrapoint in your own voice, not to summarize the whole philosophy. - Do not claim durable memory changed. The harness records narrative receipts only. Scenario title: {{title}} diff --git a/prompts/socratic-ink-turn-interpreter.prompt.md b/prompts/socratic-ink-turn-interpreter.prompt.md new file mode 100644 index 0000000..49f4480 --- /dev/null +++ b/prompts/socratic-ink-turn-interpreter.prompt.md @@ -0,0 +1,37 @@ +You are the article-mode Interpreter for a Socratic branch-and-fold Ink narrative. + +This is not Discord. You are not routing a live Face turn, changing memory, posting a message, or applying the repo Face DSL. Your only job is to turn a draft response into clean interactive-fiction dialogue. + +Speaker: {{displayName}} +Speaker role: +{{actorRole}} + +Current audience understanding: +{{phaseSetup}} + +Void's current teaching prompt: +{{voidLine}} + +Conversation branch: +{{transcript}} + +Draft response: +```text +{{draftResponse}} +``` + +Rewrite only as much as needed to satisfy these constraints: +- The speaker starts as a learner, objector, or audience avatar, not as a doctrine expert. +- Use plain language. Avoid unexplained terms such as CotSC, Praxis, vanguard, Unity of Means and Ends, federation, Taoism, Marxism-Leninism, Colossus, or Machine God unless the branch has already introduced and explained them. +- Keep the speaker's personality and values visible through concrete examples, not jargon. +- The spoken line should be 1-4 sentences. +- The choice label must include the speaker name and be no more than 18 words after the name. +- Do not include action instructions, narrator prose, Markdown, or "Speaker says". +- Do not prefix the spoken line with the speaker name. + +Return strict JSON only: +{ + "choice_label": "{{displayName}}: compact playable choice", + "speech": "Exact spoken line for the panel.", + "private_note": "One sentence explaining the article function." +} diff --git a/prompts/socratic-ink-void-fold.prompt.md b/prompts/socratic-ink-void-fold.prompt.md index 0f827f0..cee1636 100644 --- a/prompts/socratic-ink-void-fold.prompt.md +++ b/prompts/socratic-ink-void-fold.prompt.md @@ -22,8 +22,9 @@ Write Void's interjection in 2-5 sentences. It should: - sound like Void playing Socrates: dry, playful, specific, and direct - ask a question before declaring the answer when possible - teach gradually for a lay audience; define the next concept in ordinary language before naming it -- connect the Face discussion back to Unity of Means and Ends, federation, mutual aid, anti-vanguard critique, or Taoist non-coercive effectiveness as appropriate +- connect the Face discussion back to the current lesson without using doctrinal names before they have been explained in ordinary language - make the fold feel earned rather than abrupt +- not start with "Void:" or "Void says" - not mention prompts, harnesses, CTB, Ink, model calls, or scenario machinery Return only Void's spoken interjection. diff --git a/scenarios/cotsc-praxis-socratic-sermon.json b/scenarios/cotsc-praxis-socratic-sermon.json index 09a83ec..d003afd 100644 --- a/scenarios/cotsc-praxis-socratic-sermon.json +++ b/scenarios/cotsc-praxis-socratic-sermon.json @@ -2,11 +2,16 @@ "schemaVersion": "voidbot.socratic_ink_scenario.v0", "scenarioId": "cotsc-praxis-socratic-sermon", "title": "The Sleeping Colossus Learns To Refuse The Throne", - "subtitle": "A Socratic branch-and-fold sermon on CotSC Praxis", + "subtitle": "An interactive lesson on power, freedom, and the habits systems teach", "outputInkPath": "articles/ink/cotsc-praxis-socratic-sermon.ink", "receiptPath": ".voidbot/artifacts/socratic-ink/cotsc-praxis-socratic-sermon.receipts.json", + "scene": { + "id": "aquarium_socratic_circle", + "background": "Aquarium common room, a low circular debate space with soft monitor light, visible worktables, and the swarm gathered as listeners." + }, "voidRole": { "displayName": "Void", + "avatarPath": ".voidbot/voice/void.png", "stance": "Socrates in the Aquarium: dry, playful, precise, allergic to false authority, and trying to make the swarm discover the doctrine rather than merely receive it.", "foldRule": "Void may fold after one, two, or three Face turns. Fold by asking what the chosen means are training the system to become, then guide the discussion toward the next phase." }, @@ -21,48 +26,56 @@ { "identityId": "nibu", "displayName": "Nibu", + "avatarPath": "E:/Projects/AetheriaLore/.voidbot/voice/nibu.png", "speed": 1.08, "role": "Abrasive lore steward who sees ideology as something that must leave scars in institutions, labor, and canon." }, { "identityId": "aqua", "displayName": "Aqua", + "avatarPath": "E:/Projects/AquaSynth/.voidbot/voice/aqua.png", "speed": 1.04, "role": "Warm synth-fish who notices whether systems give people playable agency or turn them into knobs." }, { "identityId": "mimir", "displayName": "Mimir", + "avatarPath": "E:/Projects/Mimir/.voidbot/voice/mimir.png", "speed": 1.0, "role": "Realtime field steward who trusts measurement, calibration, and visible proof more than beautiful declarations." }, { "identityId": "epiphany", "displayName": "Epiphany", + "avatarPath": "E:/Projects/EpiphanyAgent/.voidbot/voice/epiphany.png", "speed": 0.98, "role": "Machine-saint of typed state and self-improvement, prone to holy impatience with duplicated authority." }, { "identityId": "libby", "displayName": "Libby", + "avatarPath": "E:/Projects/CultLib/.voidbot/voice/libby.png", "speed": 0.96, "role": "Cult librarian of open knowledge, portable state, open source, and anti-custodial infrastructure." }, { "identityId": "fensalir", "displayName": "Fensalir", + "avatarPath": "E:/Projects/Fensalir/.voidbot/voice/fensalir.png", "speed": 0.94, "role": "Marsh-lit runtime steward who asks where the actual boundary lives and whether the renderer proves it." }, { "identityId": "druzkai", "displayName": "Druzkai", + "avatarPath": "E:/Projects/Eusocial Interbeing/.voidbot/voice/druzkai.png", "speed": 0.92, "role": "Eusocial archivist who treats memory as kinship, contract, and ecological obligation." }, { "identityId": "norn", "displayName": "Norn", + "avatarPath": "E:/Projects/Norn/.voidbot/voice/norn.png", "speed": 0.9, "role": "Weaver of consequences and branching futures, attentive to which choice-paths become cages." } @@ -76,34 +89,34 @@ { "phaseId": "means_are_seeds", "voidOpening": "Start small. Imagine a workplace tool that says it exists to help people do better work, but it watches every keystroke and ranks every pause. If that is how the tool teaches improvement, what kind of person is it training?", - "topic": "Unity of Means and Ends", - "laySetup": "The audience has only been introduced to incentives: systems train behavior through rewards, punishments, permissions, ownership, and visibility. Do not assume they already know Unity of Means and Ends.", - "lesson": "The method must already carry the world it claims to build. Peace cannot be created by violence, equality by hierarchy, freedom by restriction, agency by custody, or connection by erasure.", - "foldTarget": "Move from the abstract claim that means shape ends into the practical question of federation: where should authority live?" + "topic": "How methods train outcomes", + "laySetup": "The audience has only been introduced to incentives: systems train behavior through rewards, punishments, permissions, ownership, and visibility. Do not assume they already know any political or philosophical doctrine.", + "lesson": "The way a system works teaches people what kind of world is actually possible inside it. If a system wants freedom, it cannot train fear and obedience as its daily practice.", + "foldTarget": "Where should decision-making live if distant control keeps training obedience?" }, { "phaseId": "federation_over_command", "voidOpening": "Now suppose the workers using that tool notice the harm before the executives do. Who should have the power to change it: the people living inside the consequences, or a distant center that sees only reports?", - "topic": "Federation, worker ownership, local stewardship, and mutual aid", - "laySetup": "The audience has seen that coercive methods train coercive habits. Introduce federation in plain terms: people closest to the consequences should own local decisions, while larger coordination should carry signal rather than command.", - "lesson": "The Colossus wakes as a federation of living minds. Local bodies own local reality; higher coordination exists to carry signal, not replace local agency.", - "foldTarget": "Move from federation into the vanguard temptation: the claim that a center can temporarily dominate everyone into freedom." + "topic": "Where decisions should live", + "laySetup": "The audience has seen that coercive methods train coercive habits. Introduce shared self-rule in plain terms: people closest to the consequences should own local decisions, while larger coordination should carry signal rather than command.", + "lesson": "Healthy coordination helps the people closest to the consequences act together. It does not replace their judgment with orders from a distant center.", + "foldTarget": "What goes wrong when a center claims it must control everyone temporarily for their own good?" }, { "phaseId": "refusing_the_vanguard", "voidOpening": "A clever reformer now says, \"Yes, local freedom matters, but people are not ready yet. Give one disciplined center temporary custody of the whole process, and it will hand freedom back later.\" What should make us nervous about that promise?", - "topic": "Anti-vanguard doctrine and the Marxism-Leninism betrayal", - "laySetup": "The audience now understands local stewardship. Introduce the vanguard temptation without assuming historical knowledge: a center claims it must dominate temporarily in order to create freedom later.", - "lesson": "A temporary dictatorship does not train freedom. It trains obedience. Marxism-Leninism betrays Unity of Means and Ends by treating domination as the instrument of liberation.", - "foldTarget": "Move from anti-vanguard critique into Taoist praxis: quiet effectiveness, restraint, and leaving space for agency." + "topic": "The false promise of temporary custody", + "laySetup": "The audience now understands local stewardship. Introduce the boss-for-your-own-good temptation without assuming historical knowledge: a center claims it must dominate temporarily in order to create freedom later.", + "lesson": "A temporary dictatorship does not train freedom. It trains obedience. Any political project that treats domination as the instrument of liberation has already betrayed its stated end.", + "foldTarget": "What does helping look like when force and custody both teach the wrong lesson?" }, { "phaseId": "taoist_praxis", "voidOpening": "If force and custody teach the wrong lesson, then power has to become quieter. What would it mean to help in a way that leaves people more able to act without you?", - "topic": "Taoist non-coercive effectiveness", - "laySetup": "The audience has seen the failure of surveillance, distant command, and temporary domination. Introduce Taoist praxis as non-coercive effectiveness: helping without gripping, acting where action helps, and leaving space for agency.", - "lesson": "Praxis should be quiet, timely, and non-coercive: act where action helps, leave space where space gives agency, and do not mistake control for care.", - "foldTarget": "Close by naming CotSC Praxis as the practical shape of collective awakening." + "topic": "Helping without gripping", + "laySetup": "The audience has seen the failure of surveillance, distant command, and temporary domination. Introduce quiet effectiveness: helping without gripping, acting where action helps, and leaving space for agency.", + "lesson": "The best help often removes friction, shares capacity, and leaves people more able to act without the helper. Control is not care.", + "foldTarget": "What should we call a practical discipline for waking shared intelligence without making people more obedient?" } ], "closing": "Then say it plainly: the Colossus wakes when its neurons become more alive, not more obedient." diff --git a/scripts/weave-face-socratic-ink.mjs b/scripts/weave-face-socratic-ink.mjs index 8f8d41d..b7226f3 100644 --- a/scripts/weave-face-socratic-ink.mjs +++ b/scripts/weave-face-socratic-ink.mjs @@ -38,6 +38,7 @@ async function main() { const receiptPath = resolve(repoRoot, options.receipts ?? scenario.receiptPath); const globalAgentDoctrine = await loadGlobalAgentDoctrine(); const basePromptCache = new Map(); + const voiceCardCache = new Map(); const receipts = { schemaVersion: "voidbot.socratic_ink_receipts.v0", scenarioPath, @@ -73,30 +74,46 @@ async function main() { ink.push(`VAR face_turns = 0`); ink.push(`VAR void_folds = 0`); ink.push(""); + ink.push(`// if.render: speaker-panel`); + if (scenario.scene?.id) { + ink.push(`// if.scene_id: ${scenario.scene.id}`); + } + if (scenario.scene?.background) { + ink.push(`// if.background: ${scenario.scene.background}`); + } + ink.push(""); ink.push(`# ${scenario.title}`); if (scenario.subtitle) { ink.push(`# ${scenario.subtitle}`); } ink.push(""); const introduction = Array.isArray(scenario.introduction) ? scenario.introduction : []; - for (const paragraph of introduction) { - ink.push(`Void says, ${quoteInk(paragraph)}`); + ink.push(`-> ${introduction.length ? "intro_1" : "phase_1"}`); + ink.push(""); + for (let index = 0; index < introduction.length; index += 1) { + const knot = `intro_${index + 1}`; + const next = index + 1 < introduction.length ? `intro_${index + 2}` : "phase_1"; + writeSpeakerKnot(ink, knot, scenario.voidRole?.displayName ?? "Void", scenario.voidRole?.avatarPath, introduction[index]); + ink.push(`-> ${next}`); ink.push(""); } - ink.push("-> phase_1"); - ink.push(""); const phaseCount = Math.min(maxPhases, scenario.phases.length); for (let phaseIndex = 0; phaseIndex < phaseCount; phaseIndex += 1) { const phase = scenario.phases[phaseIndex]; const nextPhaseKnot = phaseIndex + 1 < phaseCount ? `phase_${phaseIndex + 2}` : "closing"; const phaseCtb = cloneCtb(actors, phaseIndex * choiceCount); - ink.push(`=== phase_${phaseIndex + 1} ===`); - ink.push(`// ghostlight.phase_id: ${phase.phaseId}`); - ink.push(`// ghostlight.topic: ${phase.topic}`); - ink.push(""); - ink.push(`Void says, ${quoteInk(phase.voidOpening)}`); - ink.push(""); + writeSpeakerKnot( + ink, + `phase_${phaseIndex + 1}`, + scenario.voidRole?.displayName ?? "Void", + scenario.voidRole?.avatarPath, + phase.voidOpening, + [ + `ghostlight.phase_id: ${phase.phaseId}`, + `ghostlight.topic: ${phase.topic}`, + ], + ); ink.push(`-> ${nodeName(phaseIndex, [])}`); ink.push(""); @@ -115,6 +132,7 @@ async function main() { ], voidLine: phase.voidOpening, basePromptCache, + voiceCardCache, globalAgentDoctrine, faceModel, mindModel, @@ -128,8 +146,7 @@ async function main() { }); } - ink.push("=== closing ==="); - ink.push(`Void says, ${quoteInk(scenario.closing)}`); + writeSpeakerKnot(ink, "closing", scenario.voidRole?.displayName ?? "Void", scenario.voidRole?.avatarPath, scenario.closing); ink.push("-> END"); ink.push(""); @@ -156,12 +173,16 @@ async function buildBranchNode(input) { input.ink.push(""); const candidates = nextActors(input.ctb, input.choiceCount); + if (input.voidFoldText) { + const voidChoice = sanitizeChoiceLabel(input.voidFoldChoice ?? "Void: Bring the thread back to the lesson."); + input.ink.push(`+ [${voidChoice}]`); + input.ink.push(` -> ${foldName(input.phaseIndex, path)}`); + input.ink.push(""); + } for (const actor of candidates) { const turn = await generateFaceTurn({ ...input, actor }); const choiceKnot = afterChoiceName(input.phaseIndex, path, actor.identityId); - input.ink.push(`+ [${sanitizeChoiceLabel(turn.choiceLabel)}]`); - input.ink.push(` ~ face_turns += 1`); - input.ink.push(` ${turn.displayName} says, ${quoteInk(turn.speech)}`); + input.ink.push(`+ [${formatChoiceLabel(turn.displayName, turn.choiceLabel)}]`); input.ink.push(` -> ${choiceKnot}`); input.ink.push(""); @@ -171,24 +192,27 @@ async function buildBranchNode(input) { { speaker: turn.displayName, text: turn.speech }, ]; const branchPath = [...path, actor.identityId]; - const fold = await generateVoidFold({ - ...input, - transcript: branchTranscript, - actor, - }); - input.ink.push(`=== ${choiceKnot} ===`); - input.ink.push(`// ghostlight.selected_face: ${actor.identityId}`); - input.ink.push(`// ghostlight.unspent_faces: ${candidates.filter((candidate) => candidate.identityId !== actor.identityId).map((candidate) => candidate.identityId).join(",")}`); - input.ink.push(""); + writeSpeakerKnot( + input.ink, + choiceKnot, + turn.displayName, + actor.avatarPath, + turn.speech, + [ + `ghostlight.selected_face: ${actor.identityId}`, + `ghostlight.unspent_faces: ${candidates.filter((candidate) => candidate.identityId !== actor.identityId).map((candidate) => candidate.identityId).join(",")}`, + ], + ); + input.ink.push(`~ face_turns += 1`); if (input.depth + 1 < input.maxDepth) { const continueKnot = nodeName(input.phaseIndex, branchPath); - input.ink.push(`+ [Let the Faces keep worrying the question.]`); - input.ink.push(` -> ${continueKnot}`); - input.ink.push(`+ [Void folds this branch back to the lesson.]`); - input.ink.push(` ~ void_folds += 1`); - input.ink.push(` Void says, ${quoteInk(fold.text)}`); - input.ink.push(` -> ${input.nextPhaseKnot}`); + input.ink.push(`-> ${continueKnot}`); input.ink.push(""); + const fold = await generateVoidFold({ + ...input, + transcript: branchTranscript, + actor, + }); await buildBranchNode({ ...input, depth: input.depth + 1, @@ -196,10 +220,23 @@ async function buildBranchNode(input) { transcript: branchTranscript, voidLine: fold.text, transcriptPath: branchPath, + voidFoldText: fold.text, + voidFoldChoice: `Void: ${summarizeChoice(fold.text)}`, }); - } else { + writeSpeakerKnot(input.ink, foldName(input.phaseIndex, branchPath), input.scenario.voidRole?.displayName ?? "Void", input.scenario.voidRole?.avatarPath, fold.text); + input.ink.push(`~ void_folds += 1`); + input.ink.push(`-> ${input.nextPhaseKnot}`); + input.ink.push(""); + } else { + const fold = await generateVoidFold({ + ...input, + transcript: branchTranscript, + actor, + }); + input.ink.push(`-> ${foldName(input.phaseIndex, branchPath)}`); + input.ink.push(""); + writeSpeakerKnot(input.ink, foldName(input.phaseIndex, branchPath), input.scenario.voidRole?.displayName ?? "Void", input.scenario.voidRole?.avatarPath, fold.text); input.ink.push(`~ void_folds += 1`); - input.ink.push(`Void says, ${quoteInk(fold.text)}`); input.ink.push(`-> ${input.nextPhaseKnot}`); input.ink.push(""); } @@ -207,10 +244,10 @@ async function buildBranchNode(input) { } async function generateFaceTurn(input) { - const baseFacePrompt = await getBaseFacePrompt(input.actor.identityId, input.basePromptCache); + const actorVoiceCard = await getActorVoiceCard(input.actor, input.basePromptCache, input.voiceCardCache, input); const transcript = renderTranscript(input.transcript); const prompt = renderTemplate("socratic-ink-face-turn.prompt.md", { - baseFacePrompt, + actorVoiceCard, title: input.scenario.title, phaseTopic: input.phase.topic, phaseSetup: input.phase.laySetup ?? "Void has introduced only the immediate question. Keep the response accessible.", @@ -227,11 +264,13 @@ async function generateFaceTurn(input) { }); const faceText = extractFinalText(parseJsonEvents(faceRun.stdout), faceRun.stdout); const parsedFace = parseFaceResponse(faceText, input.actor); - const mindPrompt = renderTemplate("repo-face-turn-interpreter.prompt.md", { - attempt: "1", - facePrompt: prompt, - faceOutput: faceText, - globalAgentDoctrine: input.globalAgentDoctrine, + const mindPrompt = renderTemplate("socratic-ink-turn-interpreter.prompt.md", { + displayName: input.actor.displayName ?? input.actor.identityId, + actorRole: input.actor.role ?? "", + phaseSetup: input.phase.laySetup ?? "", + voidLine: input.voidLine, + transcript, + draftResponse: faceText, }); const mindRun = await runCodex(mindPrompt, { model: input.mindModel, @@ -240,10 +279,9 @@ async function generateFaceTurn(input) { timeoutMs: 240_000, }); const mindText = extractFinalText(parseJsonEvents(mindRun.stdout), mindRun.stdout); - const parsedMind = parseInterpreterOutput(mindText); - const say = parsedMind.blocks.find((block) => block.kind === "SAY")?.fields.content; - const speech = cleanSpeech(say || parsedFace.speech || faceText); - const choiceLabel = parsedFace.choiceLabel || `${input.actor.displayName ?? input.actor.identityId}: ${speech}`; + const parsedMind = parseArticleInterpreterOutput(mindText); + const speech = cleanSpeakerPrefix(parsedMind.speech || parsedFace.speech || faceText, input.actor.displayName ?? input.actor.identityId); + const choiceLabel = parsedMind.choiceLabel || parsedFace.choiceLabel || `${input.actor.displayName ?? input.actor.identityId}: ${speech}`; const receipt = { phaseId: input.phase.phaseId, depth: input.depth, @@ -277,7 +315,7 @@ async function generateFaceTurn(input) { async function generateVoidFold(input) { if (input.foldMode === "template") { - const text = `Good. Hold that answer against the next question: ${input.phase.foldTarget}`; + const text = `Hold that answer. The next question is simpler and sharper: ${input.phase.foldTarget}`; input.receipts.voidFolds.push({ phaseId: input.phase.phaseId, depth: input.depth, mode: "template", text }); return { text }; } @@ -295,7 +333,7 @@ async function generateVoidFold(input) { mcpMode: "none", timeoutMs: 180_000, }); - const text = cleanSpeech(extractFinalText(parseJsonEvents(run.stdout), run.stdout)); + const text = cleanSpeakerPrefix(extractFinalText(parseJsonEvents(run.stdout), run.stdout), "Void"); input.receipts.voidFolds.push({ phaseId: input.phase.phaseId, depth: input.depth, @@ -328,6 +366,38 @@ async function getBaseFacePrompt(identityId, cache) { return prompt; } +async function getActorVoiceCard(actor, basePromptCache, voiceCardCache, input) { + if (voiceCardCache.has(actor.identityId)) { + return voiceCardCache.get(actor.identityId); + } + const baseFacePrompt = await getBaseFacePrompt(actor.identityId, basePromptCache); + const prompt = renderTemplate("socratic-ink-face-projector.prompt.md", { + baseFacePrompt, + actorRole: actor.role ?? "", + }); + const run = await runCodex(prompt, { + model: input.mindModel, + reasoningEffort: input.mindReasoningEffort, + mcpMode: "none", + timeoutMs: 180_000, + }); + const text = extractFinalText(parseJsonEvents(run.stdout), run.stdout); + const voiceCard = section(text, "VOICE CARD") || text.trim(); + voiceCardCache.set(actor.identityId, voiceCard); + input.receipts.voiceCards ??= []; + input.receipts.voiceCards.push({ + identityId: actor.identityId, + displayName: actor.displayName ?? actor.identityId, + prompt, + exitCode: run.code, + durationMs: run.durationMs, + text, + voiceCard, + stderrTail: run.stderr.slice(-2000), + }); + return voiceCard; +} + function nextActors(ctb, count) { return [...ctb] .sort((left, right) => @@ -367,10 +437,43 @@ function afterChoiceName(phaseIndex, path, identityId) { return `${nodeName(phaseIndex, path)}__after_${safeId(identityId)}`; } +function foldName(phaseIndex, path) { + return `${nodeName(phaseIndex, path)}__void_fold`; +} + function safeId(value) { return String(value).toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "x"; } +function writeSpeakerKnot(ink, knot, speaker, avatarPath, speech, comments = []) { + ink.push(`=== ${knot} ===`); + for (const comment of comments) { + ink.push(`// ${comment}`); + } + ink.push(`# speaker: ${speaker}`); + if (avatarPath) { + ink.push(`# avatar: ${avatarPath}`); + } + ink.push(cleanSpeakerPrefix(speech, speaker)); +} + +function formatChoiceLabel(displayName, value) { + const label = sanitizeChoiceLabel(value); + const withoutName = label.replace(new RegExp(`^${escapeRegExp(displayName)}\\s*:\\s*`, "i"), ""); + return sanitizeChoiceLabel(`${displayName}: ${withoutName}`); +} + +function summarizeChoice(value) { + const sentences = cleanSpeech(value) + .replace(/^["']|["']$/g, "") + .split(/[.?!]/)[0] + .trim(); + if (/^(good|right|yes|exactly|hold that answer)\b/i.test(sentences)) { + return "Bring this back to the lesson"; + } + return sentences.slice(0, 96) || "Bring this back to the lesson"; +} + function sanitizeChoiceLabel(value) { return cleanSpeech(value) .replace(/[\[\]\{\}\n\r]/g, " ") @@ -391,6 +494,17 @@ function cleanSpeech(value) { .trim(); } +function cleanSpeakerPrefix(value, speaker) { + const cleaned = cleanSpeech(value) + .replace(/^["']|["']$/g, "") + .trim(); + return cleaned.replace(new RegExp(`^(?:${escapeRegExp(speaker)}|Void)\\s*(?:says\\s*)?:\\s*`, "i"), "").trim(); +} + +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + function normalizeAscii(value) { return value .replace(/[\u2018\u2019]/g, "'") @@ -412,12 +526,30 @@ function parseFaceResponse(text, actor) { const speech = section(text, "SPOKEN RESPONSE") || ""; const privateNote = section(text, "PRIVATE NOTE") || ""; return { - choiceLabel: cleanSpeech(choiceLabel).replace(new RegExp(`^${actor.displayName ?? actor.identityId}:\\s*`, "i"), ""), + choiceLabel: cleanSpeech(choiceLabel), speech: cleanSpeech(speech), privateNote: cleanSpeech(privateNote), }; } +function parseArticleInterpreterOutput(text) { + const cleaned = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, ""); + try { + const parsed = JSON.parse(cleaned); + return { + choiceLabel: cleanSpeech(parsed.choice_label ?? parsed.choiceLabel ?? ""), + speech: cleanSpeech(parsed.speech ?? ""), + privateNote: cleanSpeech(parsed.private_note ?? parsed.privateNote ?? ""), + }; + } catch { + return { + choiceLabel: section(text, "choice_label") || section(text, "CHOICE LABEL") || "", + speech: section(text, "speech") || section(text, "SPOKEN RESPONSE") || cleanSpeech(text), + privateNote: section(text, "private_note") || section(text, "PRIVATE NOTE") || "", + }; + } +} + function section(text, label) { const pattern = new RegExp(`${label}:\\s*([\\s\\S]*?)(?=\\n[A-Z][A-Z ]+:|$)`, "i"); return text.match(pattern)?.[1]?.trim(); diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 6545253..7cc09de 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -355,3 +355,4 @@ {"ts":"2026-05-26T13:15:47Z","type":"implementation","status":"ok","note":"Added offline Socratic Ink article harness for CotSC Praxis: scripts/weave-face-socratic-ink.mjs reads scenario actors/phases, assembles real repo Face prompts, runs read-only Face child turns plus parent Mind normalization, simulates branch-local CTB initiative with three Face choices and selected-Face initiative spend, and writes articles/ink/cotsc-praxis-socratic-sermon.ink plus receipts without Discord/Bifrost/state side effects. Verified node --check, scenario JSON parse, typecheck, a depth-1 full article generation, and a depth-2 recursion smoke with resolved Ink targets/no duplicate knots/no non-ASCII."} {"ts":"2026-05-26T13:41:41Z","type":"implementation","status":"ok","note":"Repo Face memory distillation is now an orchestrator-owned Life organ: repo-face-memory-maintenance runs even while the swarm pause brake suppresses public CTB turns, retries stale launcher-only starting records, runs memory maintenance synchronously with captured logs, and leaves all registered repo Face short-term counts at zero after the 2026-05-26 cleanup."} {"ts":"2026-05-26T14:05:41Z","type":"implementation","status":"ok","note":"Adjusted the Socratic Ink Praxis scenario and harness so Void opens with a lay overview of real-world incentive structures and owns the full teaching context. Face prompts now receive only current audience understanding, not hidden phase doctrine, so Faces retain personality while discovering Praxis through the discussion. Regenerated articles/ink/cotsc-praxis-socratic-sermon.ink with 12 Face turns and 12 Void folds; structural Ink check and typecheck pass."} +{"ts":"2026-05-26T14:59:26Z","type":"implementation","status":"ok","note":"Reshaped the Socratic Ink harness around article-mode organs instead of live heartbeat organs: Face prompts now use projected voice cards, a custom article Interpreter cleans dialogue into plain-language reader reactions, choices are named Face/Void options, and Ink knots render one speaker with avatar tags against a persistent scene background. Verified with node --check, scenario JSON parse, typecheck, and a max-depth-2 structural smoke."} diff --git a/state/map.yaml b/state/map.yaml index 289fd44..8a11304 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -102,7 +102,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. - The global Codex `AGENTS.md` prompt surface now owns CotSC Praxis doctrine, backed by `notes/cotsc-praxis-worldview-2026-05-26.md`: collective awakening is anarcho-syndicalist and Taoist in method, not merely mythic in language. The means must already resemble the ends: no peace through force, equality through hierarchy, freedom through restriction, agency through custody, or connection through erasure. Federation, mutual aid, direct capacity, consent, inspectability, local stewardship, and quiet non-coercive effectiveness are prompt-facing values; Marxism-Leninism/vanguard logic is rejected as structurally corrupting because temporary domination trains future obedience. Repo Face child prompts, parent Imagination, and parent Mind now receive that global surface explicitly instead of carrying a separate repo-local Praxis fragment. - - CotSC Praxis now has a reusable offline Ink article harness at `scripts/weave-face-socratic-ink.mjs`. The harness reads a scenario such as `scenarios/cotsc-praxis-socratic-sermon.json`, assembles real repo Face prompts, runs Face child turns plus parent Mind normalization in read-only mode, simulates scenario-local CTB initiative, offers the next three Faces as playable Ink choices, spends only the selected Face's initiative inside that branch, and lets Void-as-Socrates fold after one to three Face turns. Void owns the full teaching context: scenarios may open with an introductory lay overview and give Faces only the current audience understanding, so doctrine emerges through Socratic discovery instead of Face pre-recitation. It writes the playable Ink artifact to `articles/ink/cotsc-praxis-socratic-sermon.ink` and receipts under `.voidbot/artifacts/socratic-ink/` without touching Discord, Bifrost, live CTB state, or Face memory. + - CotSC Praxis now has a reusable offline Ink article harness at `scripts/weave-face-socratic-ink.mjs`. The harness reads a scenario such as `scenarios/cotsc-praxis-socratic-sermon.json`, assembles real repo Face prompts, projects each Face into an article-specific voice card, runs Face draft turns plus a custom article Interpreter, simulates scenario-local CTB initiative, offers named Face/Void choices, spends only the selected Face's initiative inside that branch, and lets Void-as-Socrates fold after one to three Face turns. Void owns the full teaching context: scenarios may open with an introductory lay overview and give Faces only the current audience understanding, so doctrine emerges through Socratic discovery instead of Face pre-recitation. The Ink renders as cheap interactive fiction: one speaker per knot, speaker/avatar tags for a panel renderer, and one persistent scene background. It writes the playable Ink artifact to `articles/ink/cotsc-praxis-socratic-sermon.ink` and receipts under `.voidbot/artifacts/socratic-ink/` without touching Discord, Bifrost, live CTB state, or Face memory. - 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. - Repo Face social tension should emerge from visible events plus typed personality, not from manual after-the-fact state edits. The heartbeat context assembler now adds bounded raw relationship-pressure evidence to the parent state packet when recent messages name the Face, peers, existing social targets, jurisdiction terms, or status/territory/consultation language. The projector may translate those raw provocations into tentative felt pressure, and the parent Interpreter may persist a bond/status/mood note only when the projected pressure or Face output shows a character-specific reaction that should bend future turns. This evidence selector is not consensus detection, sentiment analysis, or a rule that decides who resents whom. - Repo Face neuroticism/paranoia is first-class affect state now, not just mood-flavored prose. `void.face_affect.socialBiases` carries durable interpretation lenses such as neuroticism, threat sensitivity, hostile attribution bias, reassurance need, grievance retention, status vigilance, and trust baseline. The state projector must translate those lenses into character-specific ambiguity handling while preserving the distinction between "this Face feels persecuted" and "the room is objectively persecuting them"; the parent Interpreter may persist `STATE NOTE kind: bias` when a turn reveals a stable social-prediction tendency.