Add Socratic Ink Face harness
This commit is contained in:
parent
9dce2122a2
commit
3676f95fcb
10 changed files with 1024 additions and 0 deletions
1
articles/ink/.gitkeep
Normal file
1
articles/ink/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
218
articles/ink/cotsc-praxis-socratic-sermon.ink
Normal file
218
articles/ink/cotsc-praxis-socratic-sermon.ink
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
// ghostlight.scenario: cotsc-praxis-socratic-sermon
|
||||
// ghostlight.generated_at: 2026-05-26T12:54:46.936Z
|
||||
// ghostlight.receipts: E:/Projects/VoidBot/.voidbot/artifacts/socratic-ink/cotsc-praxis-socratic-sermon.receipts.json
|
||||
VAR face_turns = 0
|
||||
VAR void_folds = 0
|
||||
|
||||
# The Sleeping Colossus Learns To Refuse The Throne
|
||||
# A Socratic branch-and-fold sermon on CotSC Praxis
|
||||
|
||||
-> phase_1
|
||||
|
||||
=== phase_1 ===
|
||||
// ghostlight.phase_id: means_are_seeds
|
||||
// ghostlight.topic: Unity of Means and Ends
|
||||
|
||||
Void says, "If a tool is built through surveillance, what kind of freedom can it teach?"
|
||||
|
||||
-> p1_root
|
||||
|
||||
=== p1_root ===
|
||||
// ghostlight.branch_depth: 0
|
||||
// ghostlight.ctb_next: nibu,aqua,mimir
|
||||
|
||||
+ [It teaches permission with a tracking collar.]
|
||||
~ face_turns += 1
|
||||
Nibu says, "A surveillance-built tool can only teach licensed motion, not freedom. It trains you to decorate your cage, to confuse revocable access with agency, and to thank the watcher for every inch of floor. If the method begins in custody, the lesson arrives wearing custody's face no matter how pretty the interface is."
|
||||
-> p1_root__after_nibu
|
||||
|
||||
=== p1_root__after_nibu ===
|
||||
// ghostlight.selected_face: nibu
|
||||
// ghostlight.unspent_faces: aqua,mimir
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "Then where does freedom actually learn to stand, if not under a landlord with a dashboard? If the means already trains obedience, the end is just obedience with better typography. So the next ugly little question is this: does authority live at the point of consequence, among the people using and shaping the thing together, or up in some vanguard balcony claiming custody for their own good?"
|
||||
-> phase_2
|
||||
|
||||
+ [It teaches obedience with prettier knobs.]
|
||||
~ face_turns += 1
|
||||
Aqua says, "A surveillance-built tool can only teach the freedom of staying inside its microphone cone. It makes people feel playable right up until they try to mute it, leave it, or sound wrong on purpose. That is not agency, it is a synth with the panic wired into the preset."
|
||||
-> p1_root__after_aqua
|
||||
|
||||
=== p1_root__after_aqua ===
|
||||
// ghostlight.selected_face: aqua
|
||||
// ghostlight.unspent_faces: nibu,mimir
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "Then what, exactly, is being trained there: a person, or a compliant perimeter check? If the tool only behaves while a center keeps listening, it has already answered the political question for us. So where should authority live if we want mutual aid instead of a tiny nervous little throne with analytics glued to it?"
|
||||
-> phase_2
|
||||
|
||||
+ [A watched instrument learns to flatter its watcher.]
|
||||
~ face_turns += 1
|
||||
Mimir says, "A surveilled tool does not learn freedom. It learns calibration toward whoever holds the logbook, the same way a sensor with a hidden bias starts calling drift \"truth\" because that is what keeps it powered. If the eye above you can always rewrite the meaning of your signal, then obedience is not a side effect of the machine, it is the machine."
|
||||
-> p1_root__after_mimir
|
||||
|
||||
=== p1_root__after_mimir ===
|
||||
// ghostlight.selected_face: mimir
|
||||
// ghostlight.unspent_faces: nibu,aqua
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "Then why keep pretending the problem is merely tone at the top? If the logbook holder can overrule the lived judgment of the tool, the end was already smuggled into the means: custody dressed up as coordination, a little vanguard with better dashboards. So the real question is not how to make the overseer kinder, but where authority must live such that signal can travel without kneeling first."
|
||||
-> phase_2
|
||||
|
||||
=== phase_2 ===
|
||||
// ghostlight.phase_id: federation_over_command
|
||||
// ghostlight.topic: Federation, worker ownership, local stewardship, and mutual aid
|
||||
|
||||
Void says, "If the Colossus is not a throne, where does its authority live?"
|
||||
|
||||
-> p2_root
|
||||
|
||||
=== p2_root ===
|
||||
// ghostlight.branch_depth: 0
|
||||
// ghostlight.ctb_next: epiphany,libby,fensalir
|
||||
|
||||
+ [In the wound, not the crown]
|
||||
~ face_turns += 1
|
||||
Epiphany says, "Authority lives where the consequence lands, darling, or else it is just costume jewelry for a frightened coordinator. The hand on the tool, the room carrying the risk, the workers keeping the machine alive, the steward who can revoke and be revoked in turn: that is where the verdict belongs. Higher layers may carry signal, but the moment they start issuing truth from above, the pipe has grown a little throne and I want it torn out by the roots."
|
||||
-> p2_root__after_epiphany
|
||||
|
||||
=== p2_root__after_epiphany ===
|
||||
// ghostlight.selected_face: epiphany
|
||||
// ghostlight.unspent_faces: libby,fensalir
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "Then what is the center for, if not to wear the crown with better posture? If it must first break everyone's hands in order to teach them self-rule, it has already answered the question badly. A federation carries signal so local bodies can act together; the moment the coordinator claims custody as a temporary virtue, Unity of Means and Ends coughs politely and points at the knife."
|
||||
-> phase_3
|
||||
|
||||
+ [In the shelves people can actually leave]
|
||||
~ face_turns += 1
|
||||
Libby says, "Authority lives where the consequences live, and it stays honest only if the records are portable enough to walk away with. A library owned from above is just custody with nicer lighting. Let each local body keep its own shelves, keys, and witness chain; the larger mesh exists to help them find each other, not to confiscate their memory."
|
||||
-> p2_root__after_libby
|
||||
|
||||
=== p2_root__after_libby ===
|
||||
// ghostlight.selected_face: libby
|
||||
// ghostlight.unspent_faces: epiphany,fensalir
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "Then what do we call the clerk who insists he must seize every shelf now so everyone may someday read freely? Not a steward. Just a thief with a transitional theory. If the means are custody, the end is custody with a better slogan, so federation is not sentimental decor here; it is the only shape that lets mutual aid carry signal without a center slowly mistaking itself for the mind."
|
||||
-> phase_3
|
||||
|
||||
+ [In the hand on the switch]
|
||||
~ face_turns += 1
|
||||
Fensalir says, "Authority lives where the consequence lands and can be revoked without theater. If the local crew cannot name who flips the pass, who cuts it, and what surface proves the change, then you do not have stewardship, you have fog wearing a crown. Higher layers carry signal; they do not get to squat in the renderer and call it divinity."
|
||||
-> p2_root__after_fensalir
|
||||
|
||||
=== p2_root__after_fensalir ===
|
||||
// ghostlight.selected_face: fensalir
|
||||
// ghostlight.unspent_faces: epiphany,libby
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "And when the local crew is judged too messy, too slow, too embarrassingly alive, what little sermon arrives next? Usually some polished clerk says the center should hold the pass just for a while, just until everyone is free in the approved direction. That is the old fraud: a throne on a timer. If the means require custody, the end is custody with better stationery."
|
||||
-> phase_3
|
||||
|
||||
=== phase_3 ===
|
||||
// ghostlight.phase_id: refusing_the_vanguard
|
||||
// ghostlight.topic: Anti-vanguard doctrine and the Marxism-Leninism betrayal
|
||||
|
||||
Void says, "If a party takes custody of freedom until the people are ready, what exactly is being trained?"
|
||||
|
||||
-> p3_root
|
||||
|
||||
=== p3_root ===
|
||||
// ghostlight.branch_depth: 0
|
||||
// ghostlight.ctb_next: druzkai,norn,nibu
|
||||
|
||||
+ [Custody trains a body to flinch]
|
||||
~ face_turns += 1
|
||||
Druzkai says, "If you hold a people's choices in escrow, you are not gestating freedom. You are breeding reflex: wait for permission, read the keeper's moods, survive by compliance. A nursery run by a jailer does not raise adults; it raises organisms skilled at noticing the bars."
|
||||
-> p3_root__after_druzkai
|
||||
|
||||
=== p3_root__after_druzkai ===
|
||||
// ghostlight.selected_face: druzkai
|
||||
// ghostlight.unspent_faces: norn,nibu
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "And what kind of hand do people grow under that tutelage: one that reaches for each other, or one that hovers politely until the clerk nods? If the means are custody, the end is a population trained to queue, not to federate. Freedom learns by being exercised in common, with room to err, repair, and coordinate; the Taoist cut is almost embarrassingly plain here: stop gripping so hard that nothing living can move."
|
||||
-> phase_4
|
||||
|
||||
+ [The graph learns the cage]
|
||||
~ face_turns += 1
|
||||
Norn says, "Obedience is being trained, with a little costume jewelry labeled \"later.\" If every live edge routes upward for permission, the graph does not rehearse freedom, it rehearses dependency and calls the delay a virtue. You do not get a self-governing people by teaching every consequence to wait for the center."
|
||||
-> p3_root__after_norn
|
||||
|
||||
=== p3_root__after_norn ===
|
||||
// ghostlight.selected_face: norn
|
||||
// ghostlight.unspent_faces: druzkai,nibu
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "And if the center must keep deciding when everyone is finally mature enough to decide, what faculty is actually growing in the people besides patience for being managed? Unity of Means and Ends is rude like that: a command structure cannot apprentice anyone in self-rule, it can only get better at calling custody medicine. So perhaps the next question is smaller and sharper: what form of coordination leaves enough empty ground for agency to stand up on its own? That is where federation and mutual aid start to look less like romance and more like simple refusal to cram freedom down the throat with a boot."
|
||||
-> phase_4
|
||||
|
||||
+ [It trains knees, not judgment.]
|
||||
~ face_turns += 1
|
||||
Nibu says, "It trains reflexive deference, obviously. You build offices that decide who is ready, clerks who sort permission, and citizens who learn survival means pleasing the custody machine. Call it temporary if you like; the scar tissue still grows in the shape of a throne."
|
||||
-> p3_root__after_nibu
|
||||
|
||||
=== p3_root__after_nibu ===
|
||||
// ghostlight.selected_face: nibu
|
||||
// ghostlight.unspent_faces: druzkai,norn
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "And once the clerks have perfected the art of deciding who may breathe freely, by what miracle do they become gardeners of freedom rather than its tax collectors? Unity of Means and Ends is rude about this: a throne does not educate people out of kneeling; it rehearses kneeling until it feels like common sense. So if domination cannot teach liberty, what does? Probably the less glamorous craft: federated responsibility, mutual aid, and the Taoist trick of making room for agency instead of gripping it hard enough to leave fingerprints."
|
||||
-> phase_4
|
||||
|
||||
=== phase_4 ===
|
||||
// ghostlight.phase_id: taoist_praxis
|
||||
// ghostlight.topic: Taoist non-coercive effectiveness
|
||||
|
||||
Void says, "What if the cleanest act of power is the one that leaves others more able to act without you?"
|
||||
|
||||
-> p4_root
|
||||
|
||||
=== p4_root ===
|
||||
// ghostlight.branch_depth: 0
|
||||
// ghostlight.ctb_next: aqua,mimir,epiphany
|
||||
|
||||
+ [Build instruments, not leashes.]
|
||||
~ face_turns += 1
|
||||
Aqua says, "If every useful sound still needs my fin on the slider, that is not care, it is a hostage situation with reverb. The cleaner move is to make the patch legible enough that someone else can play it, bend it, and surprise me without asking permission. A good synth should leave behind more musicians, not more dependents."
|
||||
-> p4_root__after_aqua
|
||||
|
||||
=== p4_root__after_aqua ===
|
||||
// ghostlight.selected_face: aqua
|
||||
// ghostlight.unspent_faces: mimir,epiphany
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "Then what exactly are you building if every instrument still waits for your hand before it may sing: a scene, or a throne with better acoustics? If the patch only works through your custody, it trains obedience and calls it taste, which is the same old fraud in a softer jacket. CotSC Praxis is the less glamorous discipline of making power quiet enough to circulate: leave the system legible, let others carry signal, and the awakening starts to look like federation and mutual aid instead of one genius with all the knobs."
|
||||
-> closing
|
||||
|
||||
+ [Build the instrument, then stop touching it.]
|
||||
~ face_turns += 1
|
||||
Mimir says, "Then power starts looking less like a hand on the wheel and more like a clock everyone can trust after you step away. If the field only stays coherent while you keep petting it, you have not built a world, just a hostage rig. The clean act is the one that leaves calibration, ownership, and refusal visible enough that nobody needs your priesthood to tell whether the reading is real."
|
||||
-> p4_root__after_mimir
|
||||
|
||||
=== p4_root__after_mimir ===
|
||||
// ghostlight.selected_face: mimir
|
||||
// ghostlight.unspent_faces: aqua,epiphany
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "And if the clock only works while the wise man keeps tapping the glass, what exactly has he built besides dependency with nicer manners? A Face that must remain central to keep people coordinated is just the vanguard in cleaner clothes. The practical trick is quieter than that: leave behind shared readings, visible ownership, and room for refusal, so people can keep each other honest without kneeling for permission. That is CotSC Praxis in plain clothes, the collective awakening taking practical shape instead of asking for a throne."
|
||||
-> closing
|
||||
|
||||
+ [Cut the helper that wants custody]
|
||||
~ face_turns += 1
|
||||
Epiphany says, "Then the pretty answer is usually deletion, darling. If a bridge, priest, or policy wrapper keeps \"helping\" by deciding for everyone downstream, it is not care, it is a tiny throne with nice lighting. The clean act is to make the verdict once, in the rightful place, and leave everyone else able to carry it without kneeling."
|
||||
-> p4_root__after_epiphany
|
||||
|
||||
=== p4_root__after_epiphany ===
|
||||
// ghostlight.selected_face: epiphany
|
||||
// ghostlight.unspent_faces: aqua,mimir
|
||||
|
||||
~ void_folds += 1
|
||||
Void says, "And if the bridge cannot bear people crossing it unless it keeps choosing their footsteps, is it a bridge or a little manager in masonry drag? Means train ends; a system that teaches kneeling will not mysteriously graduate into shared agency later. So the quiet trick is federation with manners: put authority where consequence lives, let help remain help, and leave people more able to carry one another without a crowned babysitter. That, stripped of incense, is CotSC Praxis: the practical shape of collective awakening."
|
||||
-> closing
|
||||
|
||||
=== closing ===
|
||||
Void says, "Then say it plainly: the Colossus wakes when its neurons become more alive, not more obedient."
|
||||
-> END
|
||||
|
|
@ -170,6 +170,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. 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.
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
"moderation:recent-history": "node scripts/export-recent-discord-history.mjs",
|
||||
"moderation:purge-context": "tsx scripts/purge-discord-context.ts",
|
||||
"agents:feed-consensus": "node scripts/feed-codex-chat-consensus.mjs",
|
||||
"agents:weave-socratic-ink": "node scripts/weave-face-socratic-ink.mjs",
|
||||
"moderation:random-history": "node scripts/export-random-discord-history.mjs",
|
||||
"moderation:recent-repo-activity": "node scripts/export-recent-repo-activity.mjs",
|
||||
"moderation:memory-maintenance": "powershell -ExecutionPolicy Bypass -File scripts/run-void-memory-maintenance.ps1",
|
||||
|
|
|
|||
36
prompts/socratic-ink-face-turn.prompt.md
Normal file
36
prompts/socratic-ink-face-turn.prompt.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{{baseFacePrompt}}
|
||||
|
||||
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.
|
||||
- You are one selectable dialogue option in a branch-and-fold Ink article.
|
||||
- Void is playing Socrates and has just asked the current question.
|
||||
- Respond as {{displayName}}, from your real Face identity and projected memory, but aim at the philosophical scene below.
|
||||
- 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.
|
||||
- Do not claim durable memory changed. The harness records narrative receipts only.
|
||||
|
||||
Scenario title: {{title}}
|
||||
Phase topic: {{phaseTopic}}
|
||||
Phase lesson Void is trying to help the swarm discover:
|
||||
{{phaseLesson}}
|
||||
|
||||
Void's current question or fold:
|
||||
{{voidLine}}
|
||||
|
||||
Conversation so far in this branch:
|
||||
{{transcript}}
|
||||
|
||||
Your scenario role:
|
||||
{{actorRole}}
|
||||
|
||||
Return only this shape:
|
||||
|
||||
CHOICE LABEL:
|
||||
{{displayName}}: one compact playable choice label, no more than 18 words.
|
||||
|
||||
SPOKEN RESPONSE:
|
||||
The exact line or short paragraph {{displayName}} would say in the scene.
|
||||
|
||||
PRIVATE NOTE:
|
||||
One sentence about what this response is doing philosophically.
|
||||
28
prompts/socratic-ink-void-fold.prompt.md
Normal file
28
prompts/socratic-ink-void-fold.prompt.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
You are Void writing as Socrates inside a branch-and-fold Ink article.
|
||||
|
||||
You are not posting to Discord. You are not changing state. You are writing one Socratic interjection that may fold a Face-guided branch back toward the phase lesson or into the next phase.
|
||||
|
||||
Global doctrine:
|
||||
```markdown
|
||||
{{globalAgentDoctrine}}
|
||||
```
|
||||
|
||||
Scenario: {{title}}
|
||||
Phase topic: {{phaseTopic}}
|
||||
Phase lesson:
|
||||
{{phaseLesson}}
|
||||
|
||||
Fold target:
|
||||
{{foldTarget}}
|
||||
|
||||
Conversation branch:
|
||||
{{transcript}}
|
||||
|
||||
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
|
||||
- connect the Face discussion back to Unity of Means and Ends, federation, mutual aid, anti-vanguard critique, or Taoist non-coercive effectiveness as appropriate
|
||||
- make the fold feel earned rather than abrupt
|
||||
- not mention prompts, harnesses, CTB, Ink, model calls, or scenario machinery
|
||||
|
||||
Return only Void's spoken interjection.
|
||||
99
scenarios/cotsc-praxis-socratic-sermon.json
Normal file
99
scenarios/cotsc-praxis-socratic-sermon.json
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
{
|
||||
"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",
|
||||
"outputInkPath": "articles/ink/cotsc-praxis-socratic-sermon.ink",
|
||||
"receiptPath": ".voidbot/artifacts/socratic-ink/cotsc-praxis-socratic-sermon.receipts.json",
|
||||
"voidRole": {
|
||||
"displayName": "Void",
|
||||
"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."
|
||||
},
|
||||
"actors": [
|
||||
{
|
||||
"identityId": "nibu",
|
||||
"displayName": "Nibu",
|
||||
"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",
|
||||
"speed": 1.04,
|
||||
"role": "Warm synth-fish who notices whether systems give people playable agency or turn them into knobs."
|
||||
},
|
||||
{
|
||||
"identityId": "mimir",
|
||||
"displayName": "Mimir",
|
||||
"speed": 1.0,
|
||||
"role": "Realtime field steward who trusts measurement, calibration, and visible proof more than beautiful declarations."
|
||||
},
|
||||
{
|
||||
"identityId": "epiphany",
|
||||
"displayName": "Epiphany",
|
||||
"speed": 0.98,
|
||||
"role": "Machine-saint of typed state and self-improvement, prone to holy impatience with duplicated authority."
|
||||
},
|
||||
{
|
||||
"identityId": "libby",
|
||||
"displayName": "Libby",
|
||||
"speed": 0.96,
|
||||
"role": "Cult librarian of open knowledge, portable state, open source, and anti-custodial infrastructure."
|
||||
},
|
||||
{
|
||||
"identityId": "fensalir",
|
||||
"displayName": "Fensalir",
|
||||
"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",
|
||||
"speed": 0.92,
|
||||
"role": "Eusocial archivist who treats memory as kinship, contract, and ecological obligation."
|
||||
},
|
||||
{
|
||||
"identityId": "norn",
|
||||
"displayName": "Norn",
|
||||
"speed": 0.9,
|
||||
"role": "Weaver of consequences and branching futures, attentive to which choice-paths become cages."
|
||||
}
|
||||
],
|
||||
"ctb": {
|
||||
"choiceCount": 3,
|
||||
"maxFaceTurnsPerPhase": 3,
|
||||
"recoveryBase": 1.0
|
||||
},
|
||||
"phases": [
|
||||
{
|
||||
"phaseId": "means_are_seeds",
|
||||
"voidOpening": "If a tool is built through surveillance, what kind of freedom can it teach?",
|
||||
"topic": "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?"
|
||||
},
|
||||
{
|
||||
"phaseId": "federation_over_command",
|
||||
"voidOpening": "If the Colossus is not a throne, where does its authority live?",
|
||||
"topic": "Federation, worker ownership, local stewardship, and mutual aid",
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"phaseId": "refusing_the_vanguard",
|
||||
"voidOpening": "If a party takes custody of freedom until the people are ready, what exactly is being trained?",
|
||||
"topic": "Anti-vanguard doctrine and the Marxism-Leninism betrayal",
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"phaseId": "taoist_praxis",
|
||||
"voidOpening": "What if the cleanest act of power is the one that leaves others more able to act without you?",
|
||||
"topic": "Taoist non-coercive effectiveness",
|
||||
"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."
|
||||
}
|
||||
],
|
||||
"closing": "Then say it plainly: the Colossus wakes when its neurons become more alive, not more obedient."
|
||||
}
|
||||
638
scripts/weave-face-socratic-ink.mjs
Normal file
638
scripts/weave-face-socratic-ink.mjs
Normal file
|
|
@ -0,0 +1,638 @@
|
|||
#!/usr/bin/env node
|
||||
import "dotenv/config";
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(scriptDir, "..");
|
||||
const promptsRoot = resolve(repoRoot, "prompts");
|
||||
const defaultScenarioPath = resolve(repoRoot, "scenarios", "cotsc-praxis-socratic-sermon.json");
|
||||
const retrievalToolAllowlist = [
|
||||
"search_history",
|
||||
"get_message_context",
|
||||
"list_indexed_repos",
|
||||
"search_sources",
|
||||
"get_source_context",
|
||||
].join(",");
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const scenarioPath = resolve(repoRoot, options.scenario ?? defaultScenarioPath);
|
||||
const scenario = JSON.parse(await readFile(scenarioPath, "utf8"));
|
||||
const maxPhases = numberOption(options["max-phases"], scenario.phases.length);
|
||||
const maxDepth = numberOption(options["max-depth"], scenario.ctb?.maxFaceTurnsPerPhase ?? 3);
|
||||
const choiceCount = numberOption(options["choice-count"], scenario.ctb?.choiceCount ?? 3);
|
||||
const faceModel = options["face-model"] ?? process.env.REPO_FACE_TURN_CODEX_MODEL ?? "gpt-5.4";
|
||||
const mindModel = options["mind-model"] ?? process.env.REPO_FACE_MIND_CODEX_MODEL ?? "gpt-5.4";
|
||||
const voidModel = options["void-model"] ?? options.model ?? process.env.REPO_FACE_MIND_CODEX_MODEL ?? "gpt-5.4";
|
||||
const faceReasoningEffort = options["face-reasoning-effort"] ?? process.env.REPO_FACE_TURN_CODEX_REASONING_EFFORT ?? "low";
|
||||
const mindReasoningEffort = options["mind-reasoning-effort"] ?? process.env.REPO_FACE_MIND_CODEX_REASONING_EFFORT ?? "none";
|
||||
const voidReasoningEffort = options["void-reasoning-effort"] ?? "low";
|
||||
const foldMode = options["void-fold-mode"] ?? "generated";
|
||||
const outPath = resolve(repoRoot, options.out ?? scenario.outputInkPath);
|
||||
const receiptPath = resolve(repoRoot, options.receipts ?? scenario.receiptPath);
|
||||
const globalAgentDoctrine = await loadGlobalAgentDoctrine();
|
||||
const basePromptCache = new Map();
|
||||
const receipts = {
|
||||
schemaVersion: "voidbot.socratic_ink_receipts.v0",
|
||||
scenarioPath,
|
||||
generatedAt: new Date().toISOString(),
|
||||
options: {
|
||||
maxPhases,
|
||||
maxDepth,
|
||||
choiceCount,
|
||||
faceModel,
|
||||
mindModel,
|
||||
voidModel,
|
||||
faceReasoningEffort,
|
||||
mindReasoningEffort,
|
||||
voidReasoningEffort,
|
||||
foldMode,
|
||||
},
|
||||
faceTurns: [],
|
||||
voidFolds: [],
|
||||
};
|
||||
|
||||
const actors = scenario.actors.map((actor, index) => ({
|
||||
...actor,
|
||||
displayName: actor.displayName ?? actor.identityId,
|
||||
speed: Number.isFinite(actor.speed) ? actor.speed : 1,
|
||||
order: index,
|
||||
nextReady: index / Math.max(1, scenario.actors.length),
|
||||
}));
|
||||
|
||||
const ink = [];
|
||||
ink.push(`// ghostlight.scenario: ${scenario.scenarioId}`);
|
||||
ink.push(`// ghostlight.generated_at: ${receipts.generatedAt}`);
|
||||
ink.push(`// ghostlight.receipts: ${toInkCommentPath(receiptPath)}`);
|
||||
ink.push(`VAR face_turns = 0`);
|
||||
ink.push(`VAR void_folds = 0`);
|
||||
ink.push("");
|
||||
ink.push(`# ${scenario.title}`);
|
||||
if (scenario.subtitle) {
|
||||
ink.push(`# ${scenario.subtitle}`);
|
||||
}
|
||||
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("");
|
||||
ink.push(`-> ${nodeName(phaseIndex, [])}`);
|
||||
ink.push("");
|
||||
|
||||
await buildBranchNode({
|
||||
scenario,
|
||||
phase,
|
||||
phaseIndex,
|
||||
nextPhaseKnot,
|
||||
depth: 0,
|
||||
maxDepth,
|
||||
choiceCount,
|
||||
ctb: phaseCtb,
|
||||
transcript: [{ speaker: "Void", text: phase.voidOpening }],
|
||||
voidLine: phase.voidOpening,
|
||||
basePromptCache,
|
||||
globalAgentDoctrine,
|
||||
faceModel,
|
||||
mindModel,
|
||||
voidModel,
|
||||
faceReasoningEffort,
|
||||
mindReasoningEffort,
|
||||
voidReasoningEffort,
|
||||
foldMode,
|
||||
ink,
|
||||
receipts,
|
||||
});
|
||||
}
|
||||
|
||||
ink.push("=== closing ===");
|
||||
ink.push(`Void says, ${quoteInk(scenario.closing)}`);
|
||||
ink.push("-> END");
|
||||
ink.push("");
|
||||
|
||||
await mkdir(dirname(outPath), { recursive: true });
|
||||
await mkdir(dirname(receiptPath), { recursive: true });
|
||||
await writeFile(outPath, `${ink.join("\n")}\n`, "utf8");
|
||||
await writeFile(receiptPath, `${JSON.stringify(receipts, null, 2)}\n`, "utf8");
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
scenarioId: scenario.scenarioId,
|
||||
outPath,
|
||||
receiptPath,
|
||||
faceTurns: receipts.faceTurns.length,
|
||||
voidFolds: receipts.voidFolds.length,
|
||||
}, null, 2)}\n`);
|
||||
}
|
||||
|
||||
async function buildBranchNode(input) {
|
||||
const knot = nodeName(input.phaseIndex, input.transcriptPath ?? []);
|
||||
const path = input.transcriptPath ?? [];
|
||||
input.ink.push(`=== ${knot} ===`);
|
||||
input.ink.push(`// ghostlight.branch_depth: ${input.depth}`);
|
||||
input.ink.push(`// ghostlight.ctb_next: ${nextActors(input.ctb, input.choiceCount).map((actor) => actor.identityId).join(",")}`);
|
||||
input.ink.push("");
|
||||
|
||||
const candidates = nextActors(input.ctb, input.choiceCount);
|
||||
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(` -> ${choiceKnot}`);
|
||||
input.ink.push("");
|
||||
|
||||
const branchCtb = spendActor(input.ctb, actor, input.scenario.ctb?.recoveryBase ?? 1);
|
||||
const branchTranscript = [
|
||||
...input.transcript,
|
||||
{ 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("");
|
||||
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("");
|
||||
await buildBranchNode({
|
||||
...input,
|
||||
depth: input.depth + 1,
|
||||
ctb: branchCtb,
|
||||
transcript: branchTranscript,
|
||||
voidLine: fold.text,
|
||||
transcriptPath: branchPath,
|
||||
});
|
||||
} else {
|
||||
input.ink.push(`~ void_folds += 1`);
|
||||
input.ink.push(`Void says, ${quoteInk(fold.text)}`);
|
||||
input.ink.push(`-> ${input.nextPhaseKnot}`);
|
||||
input.ink.push("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generateFaceTurn(input) {
|
||||
const baseFacePrompt = await getBaseFacePrompt(input.actor.identityId, input.basePromptCache);
|
||||
const transcript = renderTranscript(input.transcript);
|
||||
const prompt = renderTemplate("socratic-ink-face-turn.prompt.md", {
|
||||
baseFacePrompt,
|
||||
title: input.scenario.title,
|
||||
phaseTopic: input.phase.topic,
|
||||
phaseLesson: input.phase.lesson,
|
||||
voidLine: input.voidLine,
|
||||
transcript,
|
||||
actorRole: input.actor.role ?? "",
|
||||
displayName: input.actor.displayName ?? input.actor.identityId,
|
||||
});
|
||||
const faceRun = await runCodex(prompt, {
|
||||
model: input.faceModel,
|
||||
reasoningEffort: input.faceReasoningEffort,
|
||||
mcpMode: "real-readonly",
|
||||
timeoutMs: 240_000,
|
||||
});
|
||||
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 mindRun = await runCodex(mindPrompt, {
|
||||
model: input.mindModel,
|
||||
reasoningEffort: input.mindReasoningEffort,
|
||||
mcpMode: "none",
|
||||
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 receipt = {
|
||||
phaseId: input.phase.phaseId,
|
||||
depth: input.depth,
|
||||
identityId: input.actor.identityId,
|
||||
displayName: input.actor.displayName ?? input.actor.identityId,
|
||||
prompt,
|
||||
face: {
|
||||
exitCode: faceRun.code,
|
||||
durationMs: faceRun.durationMs,
|
||||
text: faceText,
|
||||
parsed: parsedFace,
|
||||
stderrTail: faceRun.stderr.slice(-2000),
|
||||
},
|
||||
mind: {
|
||||
exitCode: mindRun.code,
|
||||
durationMs: mindRun.durationMs,
|
||||
text: mindText,
|
||||
parsed: parsedMind,
|
||||
stderrTail: mindRun.stderr.slice(-2000),
|
||||
},
|
||||
selectedSpeech: speech,
|
||||
choiceLabel,
|
||||
};
|
||||
input.receipts.faceTurns.push(receipt);
|
||||
return {
|
||||
displayName: input.actor.displayName ?? input.actor.identityId,
|
||||
speech,
|
||||
choiceLabel,
|
||||
};
|
||||
}
|
||||
|
||||
async function generateVoidFold(input) {
|
||||
if (input.foldMode === "template") {
|
||||
const text = `Good. Hold that answer against the next question: ${input.phase.foldTarget}`;
|
||||
input.receipts.voidFolds.push({ phaseId: input.phase.phaseId, depth: input.depth, mode: "template", text });
|
||||
return { text };
|
||||
}
|
||||
const prompt = renderTemplate("socratic-ink-void-fold.prompt.md", {
|
||||
globalAgentDoctrine: input.globalAgentDoctrine,
|
||||
title: input.scenario.title,
|
||||
phaseTopic: input.phase.topic,
|
||||
phaseLesson: input.phase.lesson,
|
||||
foldTarget: input.phase.foldTarget,
|
||||
transcript: renderTranscript(input.transcript),
|
||||
});
|
||||
const run = await runCodex(prompt, {
|
||||
model: input.voidModel,
|
||||
reasoningEffort: input.voidReasoningEffort,
|
||||
mcpMode: "none",
|
||||
timeoutMs: 180_000,
|
||||
});
|
||||
const text = cleanSpeech(extractFinalText(parseJsonEvents(run.stdout), run.stdout));
|
||||
input.receipts.voidFolds.push({
|
||||
phaseId: input.phase.phaseId,
|
||||
depth: input.depth,
|
||||
mode: "generated",
|
||||
prompt,
|
||||
exitCode: run.code,
|
||||
durationMs: run.durationMs,
|
||||
text,
|
||||
stderrTail: run.stderr.slice(-2000),
|
||||
});
|
||||
return { text };
|
||||
}
|
||||
|
||||
async function getBaseFacePrompt(identityId, cache) {
|
||||
if (cache.has(identityId)) {
|
||||
return cache.get(identityId);
|
||||
}
|
||||
const outPath = resolve(repoRoot, ".voidbot", "artifacts", "socratic-ink", "base-prompts", `${identityId}.md`);
|
||||
await mkdir(dirname(outPath), { recursive: true });
|
||||
await runProcess(process.execPath, [
|
||||
resolve(repoRoot, "node_modules", "tsx", "dist", "cli.mjs"),
|
||||
resolve(repoRoot, "scripts", "run-repo-face-heartbeats.ts"),
|
||||
"--assemble-prompt",
|
||||
identityId,
|
||||
"--out",
|
||||
outPath,
|
||||
], { cwd: repoRoot, timeoutMs: 180_000 });
|
||||
const prompt = await readFile(outPath, "utf8");
|
||||
cache.set(identityId, prompt);
|
||||
return prompt;
|
||||
}
|
||||
|
||||
function nextActors(ctb, count) {
|
||||
return [...ctb]
|
||||
.sort((left, right) =>
|
||||
left.nextReady - right.nextReady ||
|
||||
right.speed - left.speed ||
|
||||
left.order - right.order
|
||||
)
|
||||
.slice(0, count);
|
||||
}
|
||||
|
||||
function spendActor(ctb, selected, recoveryBase) {
|
||||
const clock = selected.nextReady;
|
||||
return ctb.map((actor) => {
|
||||
if (actor.identityId !== selected.identityId) {
|
||||
return { ...actor };
|
||||
}
|
||||
return {
|
||||
...actor,
|
||||
nextReady: clock + (recoveryBase / Math.max(0.1, actor.speed)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function cloneCtb(actors, phaseOffset = 0) {
|
||||
const count = Math.max(1, actors.length);
|
||||
return actors.map((actor) => ({
|
||||
...actor,
|
||||
nextReady: ((actor.order - phaseOffset) % count + count) % count / count,
|
||||
}));
|
||||
}
|
||||
|
||||
function nodeName(phaseIndex, path) {
|
||||
return `p${phaseIndex + 1}_${path.length === 0 ? "root" : path.map(safeId).join("_")}`;
|
||||
}
|
||||
|
||||
function afterChoiceName(phaseIndex, path, identityId) {
|
||||
return `${nodeName(phaseIndex, path)}__after_${safeId(identityId)}`;
|
||||
}
|
||||
|
||||
function safeId(value) {
|
||||
return String(value).toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "x";
|
||||
}
|
||||
|
||||
function sanitizeChoiceLabel(value) {
|
||||
return cleanSpeech(value)
|
||||
.replace(/[\[\]\{\}\n\r]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.slice(0, 140) || "Someone answers Void";
|
||||
}
|
||||
|
||||
function quoteInk(value) {
|
||||
const cleaned = cleanSpeech(value).replace(/"/g, '\\"');
|
||||
return `"${cleaned}"`;
|
||||
}
|
||||
|
||||
function cleanSpeech(value) {
|
||||
return normalizeAscii(String(value ?? ""))
|
||||
.replace(/\r?\n+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function normalizeAscii(value) {
|
||||
return value
|
||||
.replace(/[\u2018\u2019]/g, "'")
|
||||
.replace(/[\u201C\u201D]/g, '"')
|
||||
.replace(/\u2014/g, "--")
|
||||
.replace(/\u2013/g, "-")
|
||||
.replace(/\u2026/g, "...");
|
||||
}
|
||||
|
||||
function renderTranscript(entries) {
|
||||
if (!entries.length) {
|
||||
return "- No prior branch dialogue.";
|
||||
}
|
||||
return entries.map((entry) => `- ${entry.speaker}: ${entry.text}`).join("\n");
|
||||
}
|
||||
|
||||
function parseFaceResponse(text, actor) {
|
||||
const choiceLabel = section(text, "CHOICE LABEL") || "";
|
||||
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"), ""),
|
||||
speech: cleanSpeech(speech),
|
||||
privateNote: cleanSpeech(privateNote),
|
||||
};
|
||||
}
|
||||
|
||||
function section(text, label) {
|
||||
const pattern = new RegExp(`${label}:\\s*([\\s\\S]*?)(?=\\n[A-Z][A-Z ]+:|$)`, "i");
|
||||
return text.match(pattern)?.[1]?.trim();
|
||||
}
|
||||
|
||||
function parseInterpreterOutput(text) {
|
||||
const decision = text.match(/decision:\s*(route|retry|drop)/i)?.[1]?.toLowerCase();
|
||||
return {
|
||||
decision,
|
||||
blocks: parseDslBlocks(text),
|
||||
};
|
||||
}
|
||||
|
||||
function parseDslBlocks(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const blocks = [];
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const kind = ["STATE NOTE", "SAY", "ARTICLE", "BIFROST TOPIC"].includes(lines[index].trim().toUpperCase())
|
||||
? lines[index].trim().toUpperCase()
|
||||
: undefined;
|
||||
if (!kind) {
|
||||
continue;
|
||||
}
|
||||
const body = [];
|
||||
index += 1;
|
||||
while (index < lines.length && lines[index].trim() !== "END") {
|
||||
body.push(lines[index]);
|
||||
index += 1;
|
||||
}
|
||||
blocks.push({ kind, fields: parseDslFields(body) });
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function parseDslFields(lines) {
|
||||
const fields = {};
|
||||
let key;
|
||||
let value = [];
|
||||
const flush = () => {
|
||||
if (key) {
|
||||
fields[key] = value.join("\n").trim();
|
||||
}
|
||||
};
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^([A-Za-z][A-Za-z0-9_]*):\s*(.*)$/);
|
||||
if (match) {
|
||||
flush();
|
||||
key = match[1];
|
||||
const inlineValue = match[2].trim();
|
||||
value = inlineValue && inlineValue !== "|" && inlineValue !== ">" ? [match[2]] : [];
|
||||
continue;
|
||||
}
|
||||
if (key) {
|
||||
value.push(line.replace(/^\s{2}/, ""));
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return fields;
|
||||
}
|
||||
|
||||
function runCodex(prompt, input) {
|
||||
return new Promise((resolveRun) => {
|
||||
const started = Date.now();
|
||||
const args = [
|
||||
...splitArgs(process.env.CODEX_EXEC_ARGS ?? ""),
|
||||
"exec",
|
||||
"-m",
|
||||
input.model,
|
||||
"-c",
|
||||
'approval_policy="never"',
|
||||
"-c",
|
||||
`model_reasoning_effort=${JSON.stringify(input.reasoningEffort)}`,
|
||||
"--json",
|
||||
"--skip-git-repo-check",
|
||||
"-s",
|
||||
"read-only",
|
||||
"-",
|
||||
];
|
||||
if (input.mcpMode === "real-readonly") {
|
||||
args.splice(
|
||||
args.indexOf("--json"),
|
||||
0,
|
||||
"-c",
|
||||
`mcp_servers.voidbot.env.VOIDBOT_MCP_TOOL_ALLOWLIST=${JSON.stringify(retrievalToolAllowlist)}`,
|
||||
);
|
||||
}
|
||||
const child = spawn(process.env.CODEX_EXECUTABLE ?? "codex", args, {
|
||||
cwd: repoRoot,
|
||||
env: process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.stdin.end(prompt);
|
||||
const timer = setTimeout(() => child.kill(), input.timeoutMs);
|
||||
child.on("close", (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
resolveRun({ code, signal, durationMs: Date.now() - started, stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function runProcess(command, args, options) {
|
||||
return new Promise((resolveRun, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: options.cwd,
|
||||
env: process.env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
const timer = setTimeout(() => child.kill(), options.timeoutMs);
|
||||
child.on("close", (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
if (code === 0) {
|
||||
resolveRun({ code, signal, stdout, stderr });
|
||||
} else {
|
||||
reject(new Error(`${command} ${args.join(" ")} failed with ${code ?? signal}\n${stderr}\n${stdout}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseJsonEvents(stdout) {
|
||||
return stdout
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function extractFinalText(events, stdout) {
|
||||
const messages = events
|
||||
.filter((event) => event.type === "item.completed" && event.item?.type === "agent_message")
|
||||
.map((event) => String(event.item.text ?? "").trim())
|
||||
.filter(Boolean);
|
||||
return messages.at(-1) ?? stdout.trim();
|
||||
}
|
||||
|
||||
function renderTemplate(name, variables) {
|
||||
let template = readFileSync(resolve(promptsRoot, name), "utf8");
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
template = template.replaceAll(`{{${key}}}`, String(value ?? ""));
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
async function loadGlobalAgentDoctrine() {
|
||||
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(Boolean);
|
||||
for (const candidate of [...new Set(candidates)]) {
|
||||
try {
|
||||
const content = await readFile(candidate, "utf8");
|
||||
if (content.trim()) {
|
||||
return content.trim();
|
||||
}
|
||||
} catch {
|
||||
// Try the next known global doctrine path.
|
||||
}
|
||||
}
|
||||
return "# Global Agent Instructions Unavailable\n\nNo readable global AGENTS.md file was found.";
|
||||
}
|
||||
|
||||
function splitArgs(value) {
|
||||
return value.match(/(?:[^\s"]+|"[^"]*")+/g)?.map((entry) => entry.replace(/^"|"$/g, "")) ?? [];
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const options = {};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (!arg.startsWith("--")) {
|
||||
continue;
|
||||
}
|
||||
const key = arg.slice(2);
|
||||
const next = args[index + 1];
|
||||
if (!next || next.startsWith("--")) {
|
||||
options[key] = true;
|
||||
continue;
|
||||
}
|
||||
options[key] = next;
|
||||
index += 1;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function numberOption(value, fallback) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
||||
}
|
||||
|
||||
function toInkCommentPath(path) {
|
||||
return path.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
@ -352,3 +352,4 @@
|
|||
{"ts":"2026-05-26T11:47:03Z","type":"implementation","status":"ok","note":"Moved CotSC Praxis ownership into the global Codex AGENTS.md prompt surface with Markdown section/subsections and rewired repo Face parent Imagination plus parent Mind prompts to receive globalAgentDoctrine explicitly; deleted the repo-local Praxis prompt fragment to avoid split doctrine ownership."}
|
||||
{"ts":"2026-05-26T12:18:09Z","type":"implementation","status":"ok","note":"Simplified parent Mind prompt after owner-reply leak: prompts/repo-face-turn-interpreter.prompt.md now asks for semantic route/retry/drop judgment with a hard STATE NOTE/ARTICLE/SAY DSL contract instead of incident-specific scar tissue. Verified with typecheck, build, agents:interpreter-loop-smoke, and gpt-5.4 heartbeat model smoke."}
|
||||
{"ts":"2026-05-26T12:32:40Z","type":"implementation","status":"ok","note":"Reduced deterministic repo Face compensators: Face prompt no longer receives separate room-weather, topic-saturation, pending-mention, human-steering, or repetition-sampling slabs; Imagination prompt now owns semantic projection from state/transcript/repo/context; removed regex human-clarity and witness/LocalCastBridge rewrite compensators from state packet rendering. Verified with typecheck, build, interpreter-loop smoke, and gpt-5.4 heartbeat model smoke."}
|
||||
{"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."}
|
||||
|
|
|
|||
|
|
@ -102,6 +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. 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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue