Merge origin/main (f003801) into huginn-persona-state

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-10 08:30:51 +02:00
commit e95e2d57aa
12 changed files with 610 additions and 17 deletions

View file

@ -805,7 +805,7 @@ macro_rules! pipeline_kinds {
/// shape, not a document's.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant, reason = "a document is a transient wrapper around one value; boxing a variant would change every construction site")]
#[allow(clippy::large_enum_variant, reason = "one value per request or per batch of at most 64; boxing a variant would buy no memory and change every construction site")]
pub enum PipelineDocument { $($variant($value)),* }
impl PipelineKind {
@ -1908,7 +1908,7 @@ mod tests {
// refuses this one: the digits rule is satisfied either way.
refused(&format!("{CAMPAIGN}:cut_spec:cut-3a.1"));
refused(&format!("{CAMPAIGN}:CUT_SPEC:cut-3a.r1"));
refused(&format!("EUREKA-STATE:cut_spec:cut-3a.r1"));
refused("EUREKA-STATE:cut_spec:cut-3a.r1");
refused(&format!("{CAMPAIGN}:cut_spec:cut-3a.r1:"));
refused(&format!("{CAMPAIGN}::cut-3a.r1"));
refused("cut-3a.r1");

View file

@ -150,7 +150,7 @@ impl<S: MindStore, I: IndexSink<S>> Daemon<S, I> {
HuginnMindResponse::Admit(outcome)
}
HuginnMindRequest::View { id, .. } => match self.mind.view(&id) {
Ok(view) => HuginnMindResponse::View(view),
Ok(view) => HuginnMindResponse::View(view.map(Box::new)),
Err(refusal) => HuginnMindResponse::Refused(refusal),
},
HuginnMindRequest::PersonaGet { .. } => match self.mind.persona() {
@ -502,9 +502,11 @@ pub(crate) mod tests {
Ok(Daemon::new(Mind::open(state_root, instance)?, NoIndex))
}
type Seen = Vec<(Vec<String>, Vec<bool>)>;
#[derive(Clone, Default)]
struct RecordingIndex {
seen: Arc<Mutex<Vec<(Vec<String>, Vec<bool>)>>>,
seen: Arc<Mutex<Seen>>,
}
impl<S: MindStore> IndexSink<S> for RecordingIndex {
@ -615,7 +617,7 @@ pub(crate) mod tests {
let PipelinePageItems::Documents(views) = &page.items else { panic!("the document projection is the view") };
assert_eq!(views[0].status, PipelineStatus::InForce);
let view = daemon.handle(HuginnMindRequest::View { instance: slug(INSTANCE), id: id.clone() }, now()).answered();
assert_eq!(view, HuginnMindResponse::View(Some(views[0].clone())));
assert_eq!(view, HuginnMindResponse::View(Some(Box::new(views[0].clone()))));
}
/// Ruling 14 across the transport, on both sides: the mind refuses a read

View file

@ -596,7 +596,7 @@ impl<E: Embedder, V: VectorIndex> Projector<E, V> {
let found = self.index.search(&self.collection, &vector, limit, budget.left()?)?;
let mut hits = Vec::with_capacity(found.len());
for hit in found {
match PipelineKind::ALL.into_iter().find(|kind| kind.name() == hit.kind) {
match PipelineKind::ALL.iter().find(|kind| kind.name() == hit.kind) {
Some(kind) => hits.push((PipelineRef { kind: *kind, id: Short(hit.doc_id) }, hit.score)),
None => eprintln!("huginn: the index returned {} under the unknown kind {:?}; it is left out", hit.doc_id, hit.kind),
}

View file

@ -19,10 +19,13 @@ use super::*;
use crate::daemon::Daemon;
use crate::daemon::tests::{INSTANCE, OTHER, batch, campaign_seed, now, question_and_ruling, slug};
type AfterAbsorb = Box<dyn FnOnce(&mut Shared)>;
type Edit<T> = (&'static str, Box<dyn Fn(&mut T)>);
thread_local! {
/// Runs once, in the worker's own turn, between the inbox being absorbed
/// and a search being taken, holding the guard `serve_search` holds.
static AFTER_ABSORB: std::cell::RefCell<Option<Box<dyn FnOnce(&mut Shared)>>> = const { std::cell::RefCell::new(None) };
static AFTER_ABSORB: std::cell::RefCell<Option<AfterAbsorb>> = const { std::cell::RefCell::new(None) };
}
/// The hook `serve_search` calls at the point where a second lock once left a
@ -167,7 +170,7 @@ fn reconcile_indexes_exactly_the_indexable_documents_the_collection_lacks() {
fn a_model_digest_change_rebuilds_the_collection() {
let (_root, mind) = mind_with_documents();
let entries = mind.index_entries(None).unwrap();
let changes: [(&str, Box<dyn Fn(&mut ModelIdentity)>); 3] = [
let changes: [Edit<ModelIdentity>; 3] = [
("digest", Box::new(|identity| identity.digest = "d2".into())),
("name", Box::new(|identity| identity.name = "other-model".into())),
("dimensions", Box::new(|identity| identity.dimensions = 8)),
@ -1352,7 +1355,7 @@ fn a_model_changed_refusal_names_the_state_and_the_pending_count() {
fn a_search_re_reads_the_collections_label() {
let (_root, mind) = mind_with_documents();
let entries = mind.index_entries(None).unwrap();
let cases: [(&str, Box<dyn Fn(&mut Stored)>); 3] = [
let cases: [Edit<Stored>; 3] = [
(
"another mind's label",
Box::new(|stored| {

View file

@ -1024,7 +1024,7 @@ mod tests {
let message_id = format!("{stem}{}", "0".repeat(zeros));
let at_probe = encoded_len(&reply_with_payload(&message_id, operation, runtime_id, "A".repeat(probe)));
let payload_len = (probe as i64 + (target as i64 - at_probe as i64)) as usize;
if payload_len % 4 == 0 {
if payload_len.is_multiple_of(4) {
let message = reply_with_payload(&message_id, operation, runtime_id, "A".repeat(payload_len));
assert_eq!(encoded_len(&message), target);
return (message_id, message);

View file

@ -1751,7 +1751,7 @@ mod tests {
let hand_off_ref = r(K::HandOff, &format!("{INSTANCE}:hand_off:{OTHER_INSTANCE}.gamecult_-huginn.2026-09-16"));
// Accepted, one per resolvable kind.
committed(admit(&mut world(), vec![target(2, &[INVARIANT]), resolution(target_ref.clone(), superseded(&[target_r2.clone()]))]));
committed(admit(&mut world(), vec![target(2, &[INVARIANT]), resolution(target_ref.clone(), superseded(std::slice::from_ref(&target_r2)))]));
committed(admit(&mut world(), vec![resolution(question_ref.clone(), withdrawn())]));
committed(admit(&mut world(), vec![D::Ruling(ruling("R2")), resolution(ruling_ref.clone(), superseded(&[r(K::Ruling, &id("ruling", "R2"))]))]));
committed(admit(&mut world(), vec![resolution(spec_ref.clone(), withdrawn())]));
@ -1794,7 +1794,7 @@ mod tests {
incompatible(K::CutSpec, "Superseded")
);
assert_eq!(
refusal(admit(&mut world(), vec![resolution(finding_ref.clone(), superseded(&[finding_ref.clone()]))])),
refusal(admit(&mut world(), vec![resolution(finding_ref.clone(), superseded(std::slice::from_ref(&finding_ref)))])),
incompatible(K::Finding, "Superseded")
);
assert_eq!(
@ -2294,7 +2294,7 @@ mod tests {
committed(admit(&mut open(), vec![resolution(subject(), withdrawn())]));
let other_run = r(K::Run, &run_key("z"));
let refused = [
(superseded(&[other_run.clone()]), "Superseded"),
(superseded(std::slice::from_ref(&other_run)), "Superseded"),
(ResolutionOutcome::Answered { by: r(K::Ruling, &id("ruling", "R1")) }, "Answered"),
(ResolutionOutcome::Fixed { commit: sha(), by: None }, "Fixed"),
(ResolutionOutcome::Deferred { to: other_run }, "Deferred"),

View file

@ -120,11 +120,10 @@ impl HuginnMindRequest {
/// transport failure: the read side carries `Refused`, the write side carries
/// the outcome's own `Refused`.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[allow(clippy::large_enum_variant, reason = "one answer per request; a document view is the answer a reader asked for")]
pub enum HuginnMindResponse {
Whoami(MindStatus),
Admit(PipelineAdmissionOutcome),
View(Option<PipelineDocumentView>),
View(Option<Box<PipelineDocumentView>>),
Query(PipelineSelectionPage),
Refused(MindRefusal),
Persona(Option<PersonaStateView>),

View file

@ -485,13 +485,15 @@ mod unsafe_installs {
error.downcast::<UnsafeStore>().expect("the refusal is the typed UnsafeStore error")
}
type Case = (&'static str, u32, Option<u32>, Option<u32>, &'static str);
#[test]
fn a_group_or_world_writable_directory_lock_or_store_is_refused_and_nothing_is_written() {
if !running_as_root() {
eprintln!("SKIPPED: needs root to own the fixtures");
return;
}
let cases: [(&str, u32, Option<u32>, Option<u32>, &str); 5] = [
let cases: [Case; 5] = [
("world-writable directory", 0o777, None, None, ""),
("group-writable directory", 0o775, None, None, ""),
("world-writable lock", 0o755, Some(0o666), None, "control.cc.lock"),

View file

@ -29,6 +29,13 @@ F21. The run, Repetition breaker, Brake and Units rows changed, and the rational
Specs: `cut-run-live-admission.r1`, `cut-mind-launch.r3`, `cut-mind-wake.r3`,
`cut-brake-stop.r2`.
Revised again on 2026-10-10 by Imagination (`imagination-mind-units`, session
`self-2026-10-10-eureka`) at Huginn `0c6f6cb` and gamecult-ops `6b4a874`, for cut
`mind-units-install`. New fact F22. New section "The mind units: identity, lifecycle and
authority", written before the cut was mapped. Specs: `cut-mind-unit-gate.r1` (Huginn) and
`cut-mind-units-install.r1` (gamecult-ops). Questions: `mind-unit-names` and
`mind-unit-start-grant`.
## Pinned heads (fetched 2026-10-08 ~12:10 UTC)
| Repo | Ref | Commit | Note |
@ -217,6 +224,39 @@ Each line: what was run, where, result.
- `Slug::validate_slug` and `Label::validate_label` are public
(crates/eureka-pipeline/src/lib.rs:287, 386). `RunTurn` and `RunOperator` spell their
variants through `Debug`, which is also their `NAMES` (lib.rs:436-452).
- **F22 What the mind units would land on, 2026-10-10.** Read-only `ssh yggdrasil` as
`gamecultadmin` and an Eyes pass over Idunn `6667f0f` (Imagination `imagination-mind-units`).
- Still no unit, user or store of hers: `systemctl list-unit-files 'mind-*'` lists 0;
`getent passwd` has no `mind-*` user; `/etc/gamecult/minds` is absent; no `mind-control`
on PATH. `/opt/gamecult/huginn/current` is `releases/107552dc...` and holds only
`huginn-daemon` (0755 root). The Release step builds only `-p huginn-daemon`
(gamecult-ops `runbooks/huginn-yggdrasil.md:57`).
- systemd 257 (257.13-1~deb13u1): `ExecCondition=` (since 243) exists. Per
systemd.service(5), an ExecCondition exit of 1-254 skips the unit (inactive, not failed)
and 255 or a signal fails it; the `+` prefix runs that command with full privileges.
- **polkit is not installed:** `polkitd` "Installed: (none)", candidate 126-2;
`/etc/polkit-1/rules.d` absent (`/usr/share/polkit-1/actions` exists, from systemd).
Without polkitd, systemd refuses an unprivileged caller's `StartUnit`, so no user of hers
can start a unit today. `/etc/sudoers.d` holds only `90-gamecultadmin` and README.
- systemd cannot derive a per-instance user from a `<instance>:<label>` instance: no
specifier splits `%i`, and dash-truncated drop-ins truncate the unit prefix (the part
before `@`) and re-attach the whole instance (systemd v257 `src/shared/dropin.c`,
`unit_file_find_dirs`: `unit_name_to_prefix`, `strrchr(prefix, '-')`,
`unit_name_build_from_type(prefix, instance, ...)`). So `User=mind-%i` works only when
the instance is the mind's slug alone.
- `SystemdLauncher::alive` counts `active`, `activating` and `reloading` as alive
(mind-launch head `origin/eureka-body/mind-launch`, launch.rs:96-99), and starts with
`systemctl start --no-block`, calling `systemctl` directly (no sudo).
- Idunn has no target, binding or reference for huginn, mind-control or `mind-*`
(`/etc/gamecult/idunn/bindings/` holds ghostlight, heimdall, odin, raven-muninn and two
streampixels). Idunn supervises only workloads it admitted and launched itself as
transient `systemd-run` units (`Type=exec`, `Restart=no`, `DynamicUser=yes`; Idunn
`src/drivers.rs:3628-3650`); its continuity loop restarts an admitted workload that is not
running, up to 6 times an hour, unless its lifecycle brake is engaged
(`src/control_plane.rs` ~5880-6190, 7932-7956). It has no one-shot, timer or template
workload (`docs/watchdog-reporting-map.md` row C9) and installs no unit file, polkit rule,
sudoers file or system user (`src/drivers.rs:3620-3720`). A unit Idunn did not launch is
never observed or restarted by it.
## The model page (step 0b)
@ -237,11 +277,35 @@ pipeline kinds are in Huginn `docs/eureka-substrate-map.md`.
| Persona state | `gamecult.persona_state.v0`, one per mind, `personaId` = instance (cut-huginn-persona-state r2, eureka-substrate). | Replaced whole under CAS with a receipt; she may rewrite any field (`identity-write-all-visible`). | The Persona organ is the only writer. |
| Identity diffs | The pair of receipts of two consecutive Persona puts whose identity fields (publicName, publicDescription, presentation, values) differ. Derived, not stored. | Composed by the Persona organ at put time and sent as one delivery request through the crossing; a failed post is retried by Bifrost's delivery journal, not by her. | Persona organ composes; Bifrost posts under the permit. No rate limit. |
| Discord body surfaces | Outbound: crossing ids `epiphany.persona_discord_delivery_request.v0` and `..._permit.v0`, agent `epiphany.Persona`, runtime `epiphany-starfire` (F6), renamed to `eureka.*` and `eureka-yggdrasil` in the mouth rebinding (`mouth-crossing-on-yggdrasil`). Inbound: Bifrost persona-feedback deliveries for target `epiphany` (F6). Persona-session tools reach both (`body-includes-discord`). | Request, permit, post, receipt (Bifrost journal). Inbound deliveries: **unprobed** who records that a delivery was answered; `run-is-the-grant` forbids her organ from holding that fact. Blocks the Persona cuts only, not the cuts admitted today. | Bifrost owns posting and delivery records; her permit issuer grants while the brake is released. |
| Units and instance templates | `mind-wake@<instance>.timer/.service`, `mind-self@<instance>:<label>.service`, `mind-persona@<instance>:<label>.service` (one-shot per Persona turn; `RuntimeMaxSec` set at install bounds a hung unit), `mind-permit@<instance>`; users `mind-<instance>` (organs) and `mind-<instance>-self` (Self runs); state `/var/lib/gamecult/minds/<instance>/`; control `/etc/gamecult/minds/<instance>/`. Instance `eureka`. | Hand-installed by gamecult-ops now; Idunn targets declared before the first hosted member, which deletes the hand install (`mind-units-templated-interim`). | gamecult-ops installs; Idunn later. Starting: `open_and_launch` through `SystemdLauncher::start`, as her users, who are granted start only. The instance in a unit name is always the mind's (`MindPort::instance`), passed to every `Launcher` call; `SystemdLauncher` holds none. The instance and label are validated by the leaf's grammar before any `systemctl` argv is built. Stopping: only the operator's `brake hold`, as root (cut brake-stop). The unit-level brake gate and the start-only grant are follow-up `mind-units-brake-gate`. None of this exists on Yggdrasil yet (F19). |
| Units and instance templates | `mind-wake@<instance>.timer/.service`, `mind-self@<instance>:<label>.service`, `mind-persona@<instance>:<label>.service` (one-shot per Persona turn; `RuntimeMaxSec` set at install bounds a hung unit), `mind-permit@<instance>`; users `mind-<instance>` (organs) and `mind-<instance>-self` (Self runs); state `/var/lib/gamecult/minds/<instance>/`; control `/etc/gamecult/minds/<instance>/`. Instance `eureka`. | Hand-installed by gamecult-ops now; Idunn targets declared before the first hosted member, which deletes the hand install (`mind-units-templated-interim`). | gamecult-ops installs; Idunn later. Starting: `open_and_launch` through `SystemdLauncher::start`, as her users, who are granted start only. The instance in a unit name is always the mind's (`MindPort::instance`), passed to every `Launcher` call; `SystemdLauncher` holds none. The instance and label are validated by the leaf's grammar before any `systemctl` argv is built. Stopping: only the operator's `brake hold`, as root (cut brake-stop). The unit-level brake gate and the start-only grant are follow-up `mind-units-brake-gate`. None of this exists on Yggdrasil yet (F19, F22). Each piece's identity, lifecycle and authority: the section "The mind units" below (cut mind-units-install; unit names pending question `mind-unit-names`). |
| Self-run credential | `CLAUDE_CODE_OAUTH_TOKEN` in a root-0600 EnvironmentFile per instance. | Created by `claude setup-token`; rotated by the operator. | **Open: question `self-run-token-reach`.** The Self run's `claude` process can read its own environment, so `token-out-of-her-reach` holds only if the operator names this unit (F4 of the prior-art file, follow-up `token-reach-two-units`). |
| Wake frame | Fixed prompt frame (provenance, jurisdiction, brake obedience) in `/etc/gamecult/minds/<instance>/wake-frame.md`, root-owned. Composed by the Persona organ from host config; the waker passes the unit only the instance and the run label. | Changed only by an install. | Host config (ruling `self-waker` rider 2); never a file her runs merge into. |
| Epiphany residue | F5, F8, F11, as they stood before the teardown. | Teardown landed (gamecult-ops `9c12adb`); what it did not name is follow-up `eureka-body:follow_up:epiphany-body-leftovers`; the repo is archived last. | gamecult-ops for host and repo tooling; Idunn for its legacy target list (follow-up). |
## The mind units: identity, lifecycle and authority
Written 2026-10-10 before cut `mind-units-install` was mapped (operator direction: settle
ownership first). `<i>` is the mind's slug (`eureka` now). Unit names follow the
recommended option of question `mind-unit-names` (`instance-only`): the instance is the slug
alone, one unit per instance and turn, because admission already allows only one live run of
hers per turn (`one-live-self-run`) and only then can `User=` name a per-instance user (F22).
"Root" means the operator through `sudo`, or an agent under her deploy instruction.
| Thing | Identity | Lifecycle | Who may start, stop, enable or change it |
| --- | --- | --- | --- |
| Waker template | `/etc/systemd/system/mind-wake@.service` and `mind-wake@.timer`, sources `systemd/` in gamecult-ops. Instance unit `mind-wake@<i>.service`, `User=mind-%i`. | Installed and replaced by `scripts/install-mind-units-yggdrasil.sh`. The timer is a fixed poll (1 min after the last cycle ends); it decides nothing: the cadence is the dial's, read by the waker from the newest Persona run (mind-wake r3). Schedules and rest (follow-up `burn-schedule-and-rest`) belong in the dial, so they need no unit change. Each tick runs one cycle, `Type=oneshot`, `TimeoutStartSec=5min`, `Restart=no`. Not gated at the unit: the waker reads the brake itself and returns `Held` before any consequence, and a second reading would protect nothing. | Install, change: root via the script. Enable or disable `mind-wake@<i>.timer`: root only, by hand in the runbook. The script never enables it, so installing starts nothing. Start the service: the timer (systemd as root). Her users have no grant on `mind-wake@`. |
| Persona template | `/etc/systemd/system/mind-persona@.service`. Instance unit `mind-persona@<i>.service`, `User=mind-%i`, `Type=exec`, no `[Install]`, so it cannot be enabled. | Inactive until started for one Persona turn. Ends by its own exit, at `RuntimeMaxSec=30min`, or by the brake hold's stop (`TimeoutStopSec=30s`, then SIGKILL to the whole cgroup). `Restart=no`, and nothing else restarts it. Every start first runs the gate (below); while the brake is not Released the unit is skipped and ends inactive. `ExecStart` names the Persona organ's binary in the Huginn release; that cut ships the binary and owns the argv. Until then a released start fails at exec (203/EXEC), and the closing launch counts it as a dead run. | Start: `mind-<i>` only, through `open_and_launch` (the waker), by polkit's verb `start` on this one unit name. Stop, kill, restart: root only, which in practice means `mind-control brake hold` (cut brake-stop). Change: root via the script. Enable: nobody. |
| Self template | `/etc/systemd/system/mind-self@.service`. Instance unit `mind-self@<i>.service`, `User=mind-%i-self`, `Type=exec`, no `[Install]`. | As the Persona unit, with `RuntimeMaxSec=4h`. `ExecStart` names the Self-run actuator's binary in the Huginn release; that cut adds the token `EnvironmentFile` (ruling `self-run-token-reach`), the state directory and the argv. | Start: `mind-<i>` only (the Persona's start-Self-run tool through `open_and_launch`), by polkit, verb `start`. Stop, kill, restart: root only. Change: root via the script. Enable: nobody. |
| Organ user | `mind-<i>`: system user and group, shell `/usr/sbin/nologin`, home `/nonexistent`. | Created by the install script if absent and never modified by it after that. Deleted by root at the mind's retirement. Runs the waker and Persona units. | Root creates and deletes it. It may start `mind-persona@<i>` and `mind-self@<i>`, and nothing else. It cannot write the control store (root 0644, dir 0755) or stop a unit. It can signal its own processes with `kill(2)`, as any Unix user can. That only ends spend, and the ended run is closed Recorded by the next launch, so it adds no second stopper of her units. |
| Self user | `mind-<i>-self`: system user and group, nologin, home `/nonexistent`. The install script refuses an instance slug ending in `-self`, so this name never collides with another mind's organ user. | As the organ user. | Root creates and deletes it. It starts no unit. Polkit refuses it every verb, and the rule refuses any subject whose name ends in `-self`. |
| `mind-control` | `/opt/gamecult/huginn/releases/<sha>/mind-control`, root:root 0700, reached by the `current` symlink; `/usr/local/sbin/mind-control` links to `/opt/gamecult/huginn/current/mind-control`. | Built and installed with each Huginn release by the Release step of `runbooks/huginn-yggdrasil.md`. Replaced when `current` flips. The previous release is kept until the new one verifies. | Install, replace: root via the Release step. Run: root only (mode 0700), which covers the operator's verbs and the gate, which systemd runs as root through the `+` prefix. No user of hers can execute it. |
| `mind-wake` | `/opt/gamecult/huginn/releases/<sha>/mind-wake`, root:root 0755. | Same release step and lifecycle as `mind-control`. | Install, replace: root. Run: the waker unit as `mind-<i>`. |
| The gate | `ExecCondition=+/opt/gamecult/huginn/current/mind-control --instance %i check` in both spending templates. The verb is cut mind-unit-gate. | Runs at every start of a Persona or Self unit. It exits 0 only when `read_effective` is `Released`; held, absent, undecodable or out of bounds exit 2 (systemd skips on 1-254). It holds no state and writes nothing. | Owner: `read_effective`, the one derivation. Change: a Huginn release (the verb) or the script (the line). It closes the window in which a launch that read Released before a hold starts its unit after the hold's stop (follow-up `mind-units-brake-gate`). |
| Polkit rule | `/etc/polkit-1/rules.d/60-mind-units.rules`, root 0644, source `polkit/60-mind-units.rules` in gamecult-ops. Needs package `polkitd` (absent today, F22; question `mind-unit-start-grant`). | Installed and replaced by the script. Instance-generic: one file serves every mind. | Root changes it. It grants exactly one thing: action `org.freedesktop.systemd1.manage-units`, verb `start`, unit `mind-persona@<i>.service` or `mind-self@<i>.service`, to subject `mind-<i>`. Every other request falls through to systemd's default, `auth_admin`, which an unattended caller cannot satisfy. |
| Control store | `/etc/gamecult/minds/<i>/control.cc`, root:root 0644 in a 0755 dir (cut mind-control-store). | The install script writes `brake hold` once, if no store exists, so a fresh install reads `held` with a name and time rather than `absent`. Afterwards only the operator's verbs change it. | Operator only (`brake-dial-root-store-now`). |
| A unit on a brake hold | The instance unit above. | `brake hold` writes the brake held, then stops both units of the instance and waits; the unit ends inactive, its run stays in force. A start after the hold is skipped by the gate and ends inactive (dead), not failed. The next `open_and_launch` of that turn after a release closes the run Recorded "unit not active" (mind-launch r3 step 0). Nothing restarts a stopped unit: `Restart=no`, the waker does not launch without a Grant, and Idunn does not know the unit. | Stopper: the hold only. Restarter: none. |
| Idunn's view | No binding, no target. | Idunn observes and restarts only workloads it launched itself, and every Idunn workload is a resident service that continuity restarts. It has no one-shot, timer or template kind (F22). So today it never touches these units. The deletion line of the hand install (ruling `mind-units-templated-interim`: Idunn targets before the first hosted member) needs an Idunn job workload that is never continuity-restarted. That is follow-up `idunn-job-workload`. | Idunn: none until that follow-up lands. Its owner then may install and change the templates and users, and must never start or restart a spending unit. |
## Rationale
### Why the order is leaf, teardown, control, wake

View file

@ -332,3 +332,52 @@ Rejected: renaming the crate to `huginn-client`, for the churn above. A whoami
version field, for the reasons in the skew rule row. A config file for the
endpoint, because env already serves both hosts. TOML or YAML as the input
door: see the question.
## A campaign's repo set changes by target revision (Imagination, 2026-10-10)
Felt gap: `eureka-body:follow_up:body-campaign-repos`. eureka-body must change
GameCult/Idunn and the connector kit repo (ruling
`eureka-body:ruling:connector-kit-home`, not yet created), but `repos` lives on
`<c>:campaign:self`, a fixed key with no revision and no resolution, and
admission refuses a cut_spec whose repo is outside it (`admission.rs:405-415`,
`RepoNotInCampaign`). The only workarounds were a sibling campaign (done once:
`eureka-substrate:follow_up:epiphany-cuts-need-a-campaign`, closed by opening
eureka-body) or a cross-campaign `first` line. Pinned at Huginn a7cc2ff.
### Model page: the repo set (step 0b)
| Question | Answer | Why |
|---|---|---|
| What names it | `target.repos: Vec<OrgRepo>[16] = absent` on `<c>:target:r<n>`. Empty means "no target in this chain has named repos": the campaign's own `repos` stands. Non-empty means this revision's list is the whole set. | Reuses the one revisable campaign-scoped record. The target already carries scope (`not_in_scope`, `canonical_implementations`); which repos a campaign may cut is scope. No new kind, key grammar, alias or refusal variant. |
| Who owns it | The in-force target when it names repos; otherwise the campaign document. `campaign.repos` is demoted to the opening set: it decides only until the first target revision that names repos. | One owner at any moment, derived in one function (`Docs::repos_in_force`), read by the one rule that consumes it. |
| Add or remove | Self admits target `r<n+1>` with the new whole list and the `Superseded` resolution of `r<n>`, in one batch (the existing revision rule). The target doc cited by `doc` says why. | Whole-list replacement, as Kubernetes replaces a whole spec per generation; no per-member add/remove events to fold. |
| Silent revert | A target revision with empty `repos` whose predecessor named repos is refused `EmptyRepos { campaign: <target id> }`. | Otherwise omitting the field on an unrelated invariant edit would revert to the opening set. Reuses the existing variant: an empty repo set where one is required. |
| Bound | 16 on the target; the campaign's 8 is unchanged. | eureka-body reaches 8 with the two repos it needs. 16 matches `canonical_implementations`. The campaign bound need not widen, since later sets live on targets. |
| Specs on a removed repo | Membership is checked when a cut_spec (new or a revision) is admitted, against the set in force then. A spec already in force stays in force; its reports admit (they check the spec, not the campaign); it can be Withdrawn or Superseded as before. A new revision on the removed repo is refused `RepoNotInCampaign`. | Admission rules apply to new batches only (target invariant `stored-documents-valid`). Refusing the removal while specs name the repo would need a landed/open distinction the mind does not have (`gap-landing-untyped`), and would force withdrawing landed specs, which misstates them. Question `campaign-repo-removal`. |
| Rename or transfer | A new `Org/Repo` spelling is a new repo: Self revises the target replacing the name. Old specs keep the old name as history; a spec revision may name the new repo (the revision rule does not compare `repo`). Case changes are no-ops (`OrgRepo` equality is by identity). The host (forge or GitHub, `body-repos-on-forge`) is not in `OrgRepo` and stays out of scope. | |
| Who decides | Self admits targets. Admission checks the revision rule, distinctness by identity (the campaign's hook, shared), the bound, and the revert rule. It checks no stewardship (ruling `stewardship-rule`). | |
| Epoch | Stays `epiphany.pipeline.epoch.v2`. The field is `= absent`: stored targets decode with it empty and re-encode byte for byte; one golden line is appended. | Precedent: cut leaf-read-anchors (`reads`, `symbol`). An old daemon reading a target that names repos drops them (serde ignores unknown keys), so after a repo revision is admitted a rollback narrows the set to the opening one; it does not fail to read. This lands with the upgrade rerun waiting on `snapshot-test-stale-and-quadratic`, daemon-first (`huginn-upgrade-daemon-first`). |
### Authority map
- Owner: Huginn admission (`check`, CutSpec arm) through `Docs::repos_in_force`.
- Inputs: the campaign document; the campaign's targets and their resolutions (image and batch).
- Outputs: `RepoNotInCampaign` or acceptance for a cut_spec; `EmptyRepos` for a reverting target.
- Derived state: `campaign.repos` after a repo-naming target is display-only history (the campaign header's `repos` fact and its `repo` alias answer "opened with"). The skill's recipe reads the set in force from the target.
- Forbidden writers: any second membership check (a `campaign.repos.contains` outside `repos_in_force`); editing or re-admitting `campaign:self`.
- Shared paths: MCP admit, the CultNet wire and the stored payload decode `repos` through the same derived impls.
- Deletion line: the direct `campaign.repos.contains(&spec.repo)` at `admission.rs:413`.
### Rejected
- A per-repo record kind (`<c>:campaign_repo:<repo>.n<seq>`, Withdrawn to remove), the stewardship pattern: a new kind, schema, key grammar, alias and refusals, and its key would inherit the opaque repo encoding of `gap-stewardship-id-opaque`.
- A campaign revision (`<c>:campaign:r<n>`): re-keys every stored campaign or adds a second key grammar for one kind, and every `source` citing `<c>:campaign:self` would have to follow it.
- Status quo (sibling campaigns): splits one target's work across two targets and two invariant sets.
### Cut order
campaign-repos (Huginn; independent of clippy-pin; textual overlaps with
mind-launch in admission.rs, docs.rs and the response schema, so whichever
merges second rebases and regenerates the schema) -> skill-campaign-repos
(Eureka campaign-state.md). huginn-cli's campaigns recipe, if it prints repos,
prints the opening set until it reads the target too.

View file

@ -0,0 +1,470 @@
# Prose knowledge: target
Serves `eureka-body:follow_up:prose-knowledge-inventory` under operator ruling
`eureka-body:ruling:prose-knowledge-into-mind` (2026-10-10). The operator's
words are in the ruling; read them there. Written 2026-10-10 by Imagination
(`imagination-prose-knowledge`, session `self-2026-10-10-eureka`).
Inputs, read this pass:
- Census: `gamecult-ops/docs/research/prose-knowledge-census-2026-10-10.md`
(82 of 2,872 in-scope files opened; cited below as "census §n").
- Prior art: `gamecult-ops/docs/research/agent-knowledge-store-prior-art.md`
(cited as "PA §n" or "PA failure n"; its [V] and [M] tags are carried, and
every [M] claim this target relies on is flagged where it is used).
- The mind's kinds: `~/.claude/skills/eureka/references/campaign-state.md`.
This is a target and a backlog. It is not a migration plan and changes no code.
## 1. Pre-implementation statement
- **Objective.** Rulings, questions, owed work and ideas that live in prose
become findable by any agent of the collective mind, and nothing that decides
a campaign lives in two places.
- **Current mechanism.** The mind holds 13 kinds of typed pipeline documents
(3,344 documents at `whoami` this pass). Everything else is `.md` files in
three places with different reach: committed repo docs, which voidbot indexes
(§4); loose files at `F:\Projects\` root and in worktrees, which nothing
indexes (census §3.1); and Claude Code memory directories, which load only
into sessions whose working directory matches (census §1).
- **Invariants.**
- `one-home`: a ruling, question, owed item or fact lives in exactly one
place. Every other place carries a pointer (a mind id or a path and line),
never a restatement.
- `operator-words-typed`: an operator ruling that steers work is a `ruling`
document carrying the operator's words. Self admits it; no other faculty
admits rulings.
- `prose-is-retrievable`: prose that stays prose is reachable by one search
path that returns the repo, path and line range.
- `no-bulk-extraction`: no unattended extraction pass writes into the mind.
Each item is read, checked against the mind and admitted by a faculty.
- **Intended change.** Ownership becomes simpler: typed kinds own decisions and
owed work; prose owns explanation. Root files and memory files stop being
owners.
- **Cut line.** Root `F:\Projects\*.md` files as a place where knowledge lives;
handoff next-action lists as a store of owed work; memory files as a store of
operator rulings.
- **Subtraction budget.** No new index, store, service or daemon. One new kind
only, the one already owed (`observation`, §2.6). Retrieval reuses voidbot's
existing index; coverage is bought by moving files into indexed repos, not by
indexing more places.
## 2. The model page
One row per class the census found. "Kind" names an existing pipeline kind, a
kind earned under the doctrine's bar (owner, live consumer, invariant, liability
replaced), or "prose, retrievable".
| Class | Identity | Lifecycle | Authority | Home |
|---|---|---|---|---|
| Operator rulings and decisions | `<campaign>:ruling:<label>` | admitted once; superseded, never edited | Self admits; the operator's words in `operator_quote` | `ruling` (exists) |
| Open questions | `<campaign>:question:<label>` | open until a ruling answers it or it is withdrawn | Imagination raises; operator rules | `question` (exists) |
| Follow-ups, TODOs, owed work | `<campaign>:follow_up:<label>` | open until Fixed, Superseded or Withdrawn | Imagination or Self admits; the closer resolves | `follow_up` (exists) |
| Plans and roadmaps | campaign slug | a campaign's target and its cut specs | Self opens; Imagination maps | `campaign`, `target`, `cut_spec` (exist); repo roadmaps without a campaign stay prose |
| Ideas and design sketches | path | edited freely | the repo | prose, retrievable; a `follow_up` points at it once someone directs the work |
| Body facts and probe results | `<campaign>:observation:<label>` (proposed) | admitted with an as-of; superseded by a newer observation | Eyes admits | `observation`, earned (§2.6) |
| Rationale and history | path | postmortems and maps are history once written | the repo | prose, retrievable |
| Reference and how-to | path | maintained with the code it describes | the repo | prose, retrievable |
| Handoffs | path at `F:\Projects\` | one per Self rotation, superseded by the next | Self | a pointer to queries (§2.9); not a kind |
| Memory files | path under `~/.claude/projects/*/memory/` | edited by Life | Life | split four ways (§2.10) |
### 2.1 Operator rulings and decisions
`ruling` fits, including operator directions that answer no question
(`answers: null`, `authority: Operator`). Prose holds rulings in three shapes:
1. **Prose campaigns that finished in prose.** On 2026-09-30 the operator ruled
that campaigns then in flight finish in their prose maps ("how are we going
to move to Huginn mid-campaign? Methinks we don't.",
`~/.claude/projects/F--Projects/memory/eureka-typed-vs-prose-campaigns.md:11-12`).
Their rulings stay in the cut maps (census §4.1 lists the ten largest:
Ghostlight play-agent 65 mentions, CultNet selection 42, fire control 39,
and so on). They are history, and they are retrievable: every one of those
repos is in voidbot's index (census §2). They are not migrated. That
operator ruling is itself only in memory; it is in the list for Self (§6).
2. **Rulings restated in prose beside a typed campaign.** Examples:
`Aetheria/docs/aetheria-release-map.md` names rulings by label in "Demo
scope", and `eureka-site/BRIEF.md:12` lists operator rulings. Where the prose
cites a ruling id it is a pointer and conforms to `one-home`. Where it
restates the ruling's substance without an id, it is a second copy.
3. **Rulings only in memory files or loose doc heads** (census §4.1). These go
to Self to admit with the operator's words (§6). Once a ruling is typed, the
memory file keeps its "how to apply" text and cites the id instead of
restating the ruling. `aetheria-ship-the-game.md:9` already does this.
### 2.2 Open questions
`question` fits when the question is a real fork in a campaign. Prose "Open
questions" sections come in two kinds that look alike:
- **Forks** (an operator choice with options): Asura Q10
(`Asura/docs/cut-map.md:1671`), Fensalir Q-F1
(`Fensalir/docs/planet-work-assimilation.md:392`), Hermodr
(`Odin/docs/hermodr-extraction-plan.md:296`). In a campaign's repo they are
admitted as questions. In a repo with no campaign they wait in prose until a
campaign exists (§5, question `prose-campaign-coverage`).
- **Inquiry** (research questions with no decision attached):
`Ghostlight/docs/research/things-to-steal.md:323`, the `Mimir/research/*`
"Open Questions" sections, the prior-art "Gaps" sections. These stay prose.
A question with no options and no decider is not the `question` kind.
### 2.3 Follow-ups, TODOs, owed work
`follow_up` fits. The prose shapes are postmortem "Open follow-ups" tables,
handoff next-action lists, runbook "follow-up" lines and Soul or Eyes passes
that say "Self admits these" (census §4.3). This is the TODO-list anti-pattern:
owed work in a list no tracker reads and no one closes. The documented
practice is to harvest it into the tracker and replace the text with a
reference (PA §5, [M]: todo-to-issue harvesters; Beads replacing markdown plan
files, [M]). Here the tracker is the mind. §7 is the first harvest.
A postmortem written after this target lists its open follow-ups by id and
does not describe them again. Eureka's `references/postmortem-template.md`
should say so. That is an edit to the Eureka skill, and it rides with
`cut-skill-huginn-cli` or the next skill cut.
### 2.4 Plans and roadmaps
No `plan` kind is earned. A plan with an owner is a campaign: its target holds
the invariants, its cut specs hold the steps, and its prose map holds rationale.
That is already the rule (`campaign-state.md`, "The prose map"). Roadmaps in
repos with no campaign (`Mimir/docs/implementation-plan.md:440`,
`Fensalir/docs/perfect-fensalir-machine-roadmap.md:512`,
`StreamPixels/docs/implementation-plan.md:110` and others, census §4.4) stay
prose and retrievable until a campaign opens. Their next steps then become cut
specs or follow-ups.
`state/map.yaml` and `state/evidence.jsonl` (in 15 and 11 repos; census §4.9)
are Epiphany-era repo state. They are not migrated. They are history, and
`ghostlight-play-agent-postmortem.md:226` records that one of them is not even
parseable YAML.
### 2.5 Ideas and design sketches
These stay prose, retrievable, with no `idea` kind. The bar fails on owner and
consumer. An idea nobody has directed has no owner who would close it and no
consumer who reads it on a schedule. A typed list of ideas would be the
free-text memory block with no status, owner or supersession (PA failure 17,
[M]) with a schema on top. Explanation is the Diataxis kind that holds
rationale and ideas (PA §5, [M]).
An idea crosses into the mind when someone directs work on it. That happens in
one of three ways: the operator says so (`Aetheria/docs/faction-territory-target.md:20-30`,
"slot it in right after fire control"), a campaign's map adopts it, or a ruling
points at it. Then a `follow_up` cites the doc by path and line, and the doc
stays the idea's home.
### 2.6 Body facts and probe results: `observation`, earned
The open follow-up `eureka-body:follow_up:eyes-observation-kind` (operator
2026-10-01: "Shouldn't eyes have write access to typed memory for its
findings?") already proposes this kind. This target confirms it meets the bar
and narrows it:
- **Owner.** `huginn-mind` admission. The leaf now lives in Huginn
(`leaf-into-huginn` landed; verdict `cut-leaf-into-huginn.s2` exists).
- **Admitted by.** Eyes. Today no kind can hold an Eyes report: `finding`
needs a Soul verdict and `follow_up` needs owed work.
- **Shape.** claim (`Line`), scope (repo, path or host), as-of (commit or UTC
time), evidence (`Evidence[]`, the type findings already use), supersedes
(an observation ref), and an optional `DocRef` to the committed report for
anything longer than a `Para`.
- **Live consumers.** Imagination and Soul already cite Eyes facts. Cut specs
cite them by `DocRef` today, which requires the report to be committed in a
repo (`campaign-state.md`, prose map: "Body facts").
- **Invariant.** A fact carries the time it was true and is superseded rather
than edited. This is the bi-temporal invalidation in Zep/Graphiti, where an
older fact is marked invalid and kept, not deleted (PA §2, [V]). It is also
the supersession rule the mind already applies to rulings.
- **Liability replaced.** The stand-in Eyes files at `F:\Projects\` root:
`EYES-*.md`, 17 `aetheria-*` files plus 10 `aetheria-map-addenda-*`, 9
`eureka-*` and others (census §4.6). None of them is version-controlled or
indexed (census §3.1).
The follow-up's owner field still names "cut leaf-into-huginn", which has
landed without the kind. Self should re-own it: an eureka-body Imagination cut
in Huginn, after `huginn-cli` lands so the kind is added once, behind the new
door.
What can happen now, before the kind exists: Eyes writes its reports into the
owning repo's `docs/research/` (or `gamecult-ops/docs/research/` when no repo
owns the topic) and commits them there, never at `F:\Projects\` root. That
alone makes them retrievable (§4). Follow-up `prose-root-files-into-repos`
moves the existing root files.
### 2.7 Rationale and history
These stay prose, retrievable: campaign map Rationale sections, postmortems,
incident logs, superseded cut maps. Retirement uses the supersession rule of
ADRs, PEPs and RFCs: the old document keeps its text and gains a pointer to its
successor (PA §6, [M]). Where a superseded doc would mislead (for example
`Epiphany/notes/fresh-workspace-handoff.md`, "paused", census §5), the fix is a
one-line status header naming the successor, not a rewrite.
Duplicates are the retrieval hazard here. A search over several copies returns
old and new versions together unless the index can filter them (PA failure 18,
[M]). The census found six large cut documents duplicated between
`Epiphany/notes/` and `Huginn/docs/history/`, and `.voidbot/birth/` copies in
CultLib and Fensalir (census §4.6). Follow-up `indexed-duplicate-copies` owns
this.
### 2.8 Reference and how-to
These stay prose: runbooks, READMEs, AGENTS.md, contracts, prompts. Reference
is the one prose kind that tracks code mechanically (Diataxis, PA §5, [M]). It
stays correct only where a generator or test checks it (PA §6, [M]: generated
docs plus a CI diff check). Nothing in this target adds such checks; they
belong to each repo's own campaign.
### 2.9 Handoffs
No kind. The `hand_off` kind exists, but it records a stewardship transfer
between minds (parked, Cut 12), and a Self rotation is not that. A rotation
handoff should hold only what the mind cannot answer:
- the area and the campaigns this Self owns;
- the session label convention;
- pointers to the recipes to run at re-entry.
Owed work goes into the mind before rotation, as follow-ups. Memory
`handoff-is-a-pointer.md` already says to query the mind rather than trust a
handoff's lists. The 2026-10-10 handoff still carries a "Host loose ends"
section and cast-art plans that were in no document (§7 admits them).
When `run-kind` lands (`epiphany.pipeline.run.v2`, eureka-substrate), "who is
working on what" becomes derivable from runs, and the handoff shrinks again.
### 2.10 Memory files
Memory files (100 of them, census §1) hold four different things, and each has
a different home:
| Content | Example | Home |
|---|---|---|
| Operator rulings | `foss-games-monetization.md`, `cultcache-stores-outside-assets.md` | `ruling`, admitted by Self (§6); the memory keeps a pointer and its how-to-apply text |
| Operator context, relationship, lessons and scars that steer judgment | `operator-rapport.md`, `probe-process-spawning.md` | Eureka's Persona state (`gamecult.persona_state.v0` in Huginn) once `cut-huginn-persona-state.r3` lands. The standard puts "hard-won operating lessons, rakes stepped on, relationship context" there (`gamecult-ops/docs/persona-state-standard.md:22-25`) |
| Reference facts about the body | `cultlib-ci-harness.md`, `mcp-server-registration.md` | the owning repo's docs or runbooks (prose, retrievable); the memory keeps a pointer |
| Cross-project pointers and the index | `MEMORY.md` | stays, as Claude Code's harness loads it |
The second row hides a fork. Claude Code auto-loads `MEMORY.md`. Huginn's
Persona state is loaded by nothing in a Claude Code session. If both hold
lessons, they are two copies. The question `memory-persona-projection` (§8)
puts the options. The recommendation is that Persona state is canonical and the
memory index becomes a generated projection, marked `provenance.authority:
projection` as the cut spec already requires of file copies. That follows the
generated-file pattern (PA §6, [M]) and must wait for the persona-state cut.
## 3. Keeping prose and typed state from becoming second copies
**The rule.** When a class is typed, the prose item becomes a pointer: the
mind id, plus at most a one-line gloss. The prose is never deleted when it
holds rationale, and never left restating the decision. This is the
single-source rule as ADR, PEP and RFC indexes practise it: one record, other
places link to it (PA §6, [M]).
**Retirement order.** First admit, then repoint the prose in the same session,
citing the admission receipt. A prose item that is done (§7 found several:
items 4, 14, 33, 39) gets nothing new. It is left as history, with a status
line if it would mislead a reader.
**Who enforces it, and how.**
- **Life** owns memory files and the doctrine surfaces. At phase boundaries it
checks that memory rulings it finds are typed, and repoints them.
- **Soul**, when a cut touches a campaign's prose map, checks it against
`campaign-state.md`'s existing rule ("no progress section, no rulings and no
ledger").
- **No regex tribunal.** Whether a paragraph restates a ruling is a question of
meaning. Published drift detectors reach 72% accuracy or 0.72 precision with
0.48 recall, and their benchmarks contain mislabelled data (PA §6 and
failure 21, [V]). Detection is therefore advisory: an agent reads, proposes,
and a faculty confirms. This is the propose-then-confirm pattern of published
tooling (PA §4, [M]).
**Why no bulk extraction.** Single-prompt extraction over a long document
under-extracts, recovering one or two decisions from an hour of discussion
(PA §4, [V]). Extraction has two silent failures, omission and invention
(PA failure 13, [V]). Constrained decoding guarantees shape, not truth (PA
failure 14, [M]). LLM-driven memory updates overwrite without review (PA
failure 16). So each item is triaged by an agent that reads the cited section,
checks the mind by semantic query (dedupe), checks the file still says it
(staleness), and admits with `locations` citing file and line. §7 is that
procedure run once over the census list.
## 4. Retrieval for prose that stays prose
**One path: voidbot's existing source index. No second index.**
What voidbot is, verified this pass:
- The service is `voidbot-retrieval.service` on Yggdrasil. Its MCP container
serves streamable HTTP on `127.0.0.1:17875/mcp`, with the tool allowlist
`list_indexed_repos, search_history, get_message_context, search_sources,
get_source_context, get_exact_source_document`
(`gamecult-ops/compose/voidbot-retrieval.yggdrasil.yaml:11-27`).
- `search_sources` takes `query`, `limit`, `repoName`, `pathPrefix` and
`language` (`VoidBot/apps/worker/src/mcp-server-shared.ts:93-99`). It
returns `repoName`, `path`, `chunkIndex`, `lineStart`, `lineEnd` and
`lastModifiedAt` for each hit (`mcp-server-shared.ts:309-321`), so an answer
can cite a path and line range. It returns no commit.
- `voidbot-source-refresh.timer` runs hourly. It mirrors the **public GameCult
GitHub catalog** into `/srv/voidbot/source-repos` and re-embeds changed
chunks (`gamecult-ops/runbooks/voidbot-retrieval-recovery-yggdrasil.md:109-121`;
the catalog URL is `scripts/refresh-voidbot-sources.sh:82`).
- Collection `voidbot_repository_source_chunks` lives in Qdrant on
`127.0.0.1:6333`. Huginn's own index uses the same Qdrant and the same
embedder and model, `qwen3-embedding:0.6b` on Raven
(`gamecult-ops/runbooks/huginn-yggdrasil.md:18-27`). The two indexes already
share their substrate. They differ in what they hold: Huginn indexes only
typed documents' named fields (`Huginn/crates/huginn-mind/src/index.rs:42`).
Who can reach it:
- On Starfire, sessions reach it through the Yggdrasil tunnel
(`gamecult-ops/scripts/start-yggdrasil-tunnel.ps1:23`,
`runbooks/yggdrasil-ssh-tunnel.md:88`). It is registered in
`~/.claude.json` as the `voidbot` HTTP MCP server.
- On Yggdrasil, a Self-run unit or the Huginn daemon reaches it on loopback.
What it does not reach, and the fix for each:
| Not indexed | Why | Fix |
|---|---|---|
| `F:\Projects\` root files (56) and non-git dirs | not in any repo | move them into the owning repo's docs and commit (`prose-root-files-into-repos`) |
| memory dirs | outside repos | the §2.10 split: rulings to the mind, reference to repo docs |
| worktrees | duplicates of indexed repos | nothing; excluding them is correct |
| private or forge-only repos, and repos missing from the catalog (`Aetheria-legacy`, `Blueberrichu`) | the catalog is public GitHub only | out of scope until a repo with live work is affected |
Rejected: a second prose index in Huginn. It would duplicate an hourly-refreshed
index over the same Qdrant and embedder, and give the corpus two owners.
Graph and tree indexes (GraphRAG, RAPTOR) were also rejected. They cost an LLM
pass over the corpus at index time and an update strategy on every change (PA
failure 11, [M]), and their reported gains fall under independent audit (PA
failure 10, [V]). Plain retrieval with citations is the baseline this corpus
needs. Letta's report that a filesystem plus grep competes with specialised
memory (PA §2, [M]) points the same way.
Two gaps in the path itself:
- **No commit in the hit.** A `DocRef` needs a commit. The fix is VoidBot's
own: add the indexed commit to the source metadata (follow-up
`voidbot-hit-commit`).
- **Which door.** The operator's direction to replace MCP with plain CLIs is
recorded against eureka-state (`eureka-substrate:follow_up:huginn-cli-replaces-mcp`).
Whether voidbot's door follows is a separate fork, put in question
`prose-retrieval-door` (§8). The recommendation is to keep voidbot's door
now; if it changes, the CLI is VoidBot's own and Huginn never proxies it.
## 5. Sequencing
| Now, with existing kinds | Waits on |
|---|---|
| §7 triage: follow-ups and a question admitted under owning campaigns | none |
| §6: Self admits the memory-only rulings with the operator's words | none |
| Eyes reports committed in repos, not at `F:\Projects\` root; root files moved (`prose-root-files-into-repos`) | none |
| Handoffs as pointers; loose ends admitted before rotation | none |
| Postmortem template lists follow-ups by id | rides the next Eureka skill cut (`cut-skill-huginn-cli`) |
| `observation` kind; then the root Eyes facts are admitted | `cut-huginn-cli` (one door to add the kind behind), then an eureka-body Imagination cut re-owning `eyes-observation-kind` |
| Memory lessons and rapport into Eureka's Persona state | `cut-huginn-persona-state.r3`, then question `memory-persona-projection` |
| Work in repos with no campaign (Kalsa, Fensalir, Mimir, AquaSynth, Odin, Muninn, StreamPixels and others) | question `prose-campaign-coverage`; a campaign spanning existing campaigns' repos waits on `eureka-body:follow_up:body-campaign-repos` (campaigns cannot gain repos) |
| Handoff lists derived from runs | `run-kind` (eureka-substrate) |
| `DocRef` from a voidbot hit | `voidbot-hit-commit` |
## 6. Rulings found only in prose or memory: for Self to admit
Self admits these with the operator's words. Imagination does not type rulings.
Each was checked against the mind by semantic query this pass; "near" names
the nearest typed ruling, which does not carry the same decision.
| Ruling (paraphrase) | Operator's words at | In the mind? |
|---|---|---|
| FOSS games are monetized as games-as-a-service, microtransactions included; the fork is the check | `~/.claude/projects/F--Projects/memory/foss-games-monetization.md:11-14` (the decision is stated in the memory's voice; the correction is narrated at `:21-22`; no verbatim quote) | no; near `eureka-body:ruling:always-open-source` |
| CultCache stores never live under Unity `Assets/`; no Unity serialization for game data | `F--Projects/memory/cultcache-stores-outside-assets.md:10` (verbatim, 2026-09-14), plus the Studio goal at `:19` | no; near `aetheria-release:ruling:ship-data-all-in-cc` |
| Spotiverse and Stonks are wanted small daemons, not corpses | `F--Projects/memory/wanted-small-daemons.md:11-18` (2026-09-06; partly quoted: "a real simple CultMesh bridge") | no |
| Having no consumer does not make code dead: park it with a tag and a restore note | `F--Projects/memory/parked-not-dead.md:11-14`, intent quote `:23` (2026-09-13) | only per instance: `ghostlight-verse:ruling:park-pre-rebuild-tree` |
| The operator's original CultCache.cs is the style bar | `F--Projects/memory/original-cultcache-style-bar.md:10` (verbatim, 2026-09-13) | not checked by query; no near ruling surfaced in the CultCache queries |
| Not going back to Codex/OpenAI as inference supplier, for alignment reasons | `F--Projects/memory/codex-retirement.md:20-23` (verbatim, 2026-09-05) | no; near `eureka-body:ruling:connector-owner` |
| AetheriaEve is preserved for taxidermy | `F--Projects/memory/aetheriaeve-taxidermy.md:10` (verbatim, 2026-09-13) | covered in substance by `variants:ruling:aetheriaeve-not-consumer` (2026-09-30); Self decides whether the quote is worth a ruling |
| Aetheria mods are compiled plugins; no scripting runtime or DSL for modders | `F--Projects-Aetheria/memory/aetheria-mods-are-compiled.md:16-18` (verbatim, 2026-10-03) | no; near `aetheria-release:ruling:modders-share-our-editor` |
| Aetheria's release bar: more ships and gear, UX polish and audio feedback before announcing | `F--Projects-Aetheria/memory/aetheria-release-bar.md:9-11` (verbatim, 2026-10-02) | no |
| The built-in pipeline is chosen; owning shaders is the point | `F--Projects-Aetheria/memory/built-in-pipeline-owned-shaders.md:10` (verbatim, 2026-10-10) | in substance `aetheria-release:ruling:shaders-own-every-pixel` (2026-10-03); the 2026-10-10 words add the reason |
| Campaigns in flight on 2026-09-30 finish in their prose maps | `F--Projects/memory/eureka-typed-vs-prose-campaigns.md:11-12` (verbatim) | no |
| Embedding moves off Nightwing because its availability "is more sporadic nowadays" | `F--Projects/memory/nightwing-embedding-migration-2026-09-26.md:12-16` | not checked |
| Faction territory: "slot it in right after fire control" | `Aetheria/docs/faction-territory-target.md:20-30` (2026-09-19) | no; the follow-up in §7 points at it |
| Negent weapons concept direction | `Aetheria/docs/negent-weapons-concept.md` head (2026-09-18) | no; the follow-up in §7 points at it |
| Asura: rulings on the cut map's questions | `Asura/docs/target.md:150` (2026-09-25) | the prose-campaign rule applies (§2.1); not migrated |
## 7. Triage of the census list (census §6)
Each item was checked in two ways. The file was read at the cited line on the
working tree (Ghostlight on `origin/main`, because local checkouts lag). The
mind was searched by semantic query. Admitted ids are under the campaign named.
"Stays" means no document was admitted, and why.
| # | Item | Result |
|---|---|---|
| 1 | Heimdall secrets exposed; rotate and move to `secret_files` | admitted `eureka-body:follow_up:heimdall-binding-secrets` |
| 2 | Backups unencrypted; key owner | admitted `eureka-body:follow_up:state-backup-encryption` (the weekly tier landed, `3d091c5`; encryption is still open, runbook `gamecult-state-backup.md:103`) |
| 3 | Raven reboot test, ESP, `reagentc`, inventory entry | admitted `eureka-body:follow_up:raven-reboot-esp-inventory` |
| 4 | Stale Raven LAN IP in inventory | done: `inventory.md:56-59` carries the 2026-09-09 IP and marks the old one superseded |
| 5 | Brush-tree campaign | no campaign repo (Brokkr, GameCult.Geometry.Csg); a proposal card exists; in question `prose-campaign-coverage` |
| 6 | Cast art: Flux portraits, shortlist | admitted `site-masthead:follow_up:cast-art-flux-portraits` |
| 7 | Faction territory generative names | admitted `aetheria-release:follow_up:faction-territory-target` |
| 8 | Negent weapons concept | admitted `aetheria-release:follow_up:negent-weapons-concept` |
| 9 | Content batch one §9 forks | admitted `aetheria-release:follow_up:content-batch-one-reconcile` (F1 resolved in the doc; the doc predates the demo rulings) |
| 10 | Asura Q10 switch probabilities | admitted question `asura:question:q10-eroded-aeolian-rate` |
| 11 | Asura dual contouring | stays: a deferred design in the target, rationale |
| 12 | Asura 4b-ii remaining terms | admitted `asura:follow_up:terrain-4b-ii-remaining-terms` |
| 13 | CultMath tape open items | admitted `cultmath-tapes:follow_up:tape-unpark-open-items` |
| 14 | Variants open forks | done: ruled in prose, `CultLib/docs/document-variants-cut.md:77-100` (2026-09-17 and 09-29) |
| 15 | TS schema id literal vs C# fingerprint | superseded by `variants:ruling:wire-id-is-version` and `variants:follow_up:wire-id-cuts` |
| 16 | Mutation testing scope | admitted `cultlib-gaps:follow_up:mutation-testing-scope` |
| 17 | CultNet distributed database next slice | admitted `cultlib-gaps:follow_up:cultnet-distdb-next-slice` (related: `variants:follow_up:golden-path-write-api`) |
| 18 | Muninn has two bodies | no campaign repo; in question `prose-campaign-coverage` |
| 19 | Muninn records out of `odin-core` | no campaign repo; same question |
| 20 | Hermodr extraction and its operator questions | no campaign repo; same question |
| 21 | Mimir next | no campaign repo; same question |
| 22 | Eve remaining work | admitted `thing:follow_up:eve-roadmap-remaining-work` |
| 23 | Heimdall gaps | no campaign repo; same question |
| 24 | Gjallar next questions | no campaign repo; same question |
| 25 | Fensalir Q-F1 | no campaign repo; same question |
| 26 | AquaSynth next pull | no campaign repo; same question |
| 27 | StreamPixels M1 | no campaign repo; same question |
| 28 | Ghostlight play-agent postmortem open follow-ups | admitted `ghostlight-verse:follow_up:play-agent-postmortem-open-items` |
| 29 | Ghostlight stock lenses L1.f1, f13, f14 | admitted `ghostlight-verse:follow_up:stock-lenses-open-items` |
| 30 | Ghostlight multiplayer social policy | stays: `ghostlight-dungeon-multiplayer-intention.md` is gone from `origin/main` (parked by `ghostlight-verse:ruling:park-pre-rebuild-tree`) |
| 31 | Ghostlight research questions | stays: inquiry, not a fork (§2.2) |
| 32 | Soul pass owed, 13 survivors | typed: `ghostlight-verse:follow_up:owed-soul-unpinned-survivors` |
| 33 | Aetheria design review | done: swept by `aetheria-release:follow_up:release-target-is-a-demo` (Fixed, `228f241e`) and `vault-docs-sweep` |
| 34 | Merge checklist open questions | Locomotion Q4 is answered by `aetheria-release:ruling:facing-separate-from-aim`; the rest is admitted as `aetheria-release:follow_up:merge-checklist-residue` |
| 35 | Vault Cleanup Q-08..Q-15 | admitted `aetheria-release:follow_up:vault-cleanup-accepted-queue` |
| 36 | Kalsa deepening queue | no campaign repo; in question `prose-campaign-coverage` |
| 37 | Zyphos next action | no campaign repo; same question |
| 38 | weksa roadmap | no campaign repo; same question (see also `eureka-body:follow_up:weksa-mind`) |
| 39 | Eureka brief lessons | done: all six landed in `references/briefs.md` (`:113`, `:369`, `:401`, `:409`, `:81`) and `SKILL.md:658` |
| 40 | Doctrine de-shout and publish scrub | de-shout landed (Eureka `352165d`); the scrub's application was not checked; both rulings are typed (`doctrine-explains-not-shouts`, `doctrine-publish-scrub`) |
| 41 | Idunn watchdog open findings | stays: the postmortem restates mind documents |
| 42 | Substrate leftovers on Yggdrasil | admitted `eureka-substrate:follow_up:huginn-107552dc-leftovers` |
| 43 | Bifrost first vertical slice | admitted `eureka-body:follow_up:bifrost-context-next-target` |
| 44 | Pomba Branca off Wix | no campaign repo; in question `prose-campaign-coverage` |
| 45 | Notaris brief questions | stays: questions for an outside notary, not forks in the mind |
| 46 | Prior-art gaps sections | stays: Eyes gaps; `observation` will hold them (§2.6) |
| 47 | Repixelizer hygiene | no campaign repo; in question `prose-campaign-coverage` |
| 48 | Spotiverse and Stonks | the ruling is in §6; the work is in question `prose-campaign-coverage` |
| 49 | Memory-only rulings | §6, for Self |
Added from this target's own mechanism, with the source set to the follow-up:
`eureka-body:follow_up:prose-root-files-into-repos`,
`eureka-body:follow_up:indexed-duplicate-copies` and
`eureka-body:follow_up:voidbot-hit-commit`.
## 8. Questions raised
- `eureka-body:question:prose-campaign-coverage`: which repos with no campaign
get one.
- `eureka-body:question:memory-persona-projection`: Persona state or
`MEMORY.md` as the home of lessons.
- `eureka-body:question:prose-retrieval-door`: whether voidbot's MCP door
changes.
Each carries options and a recommendation in the mind.

4
rust-toolchain.toml Normal file
View file

@ -0,0 +1,4 @@
[toolchain]
channel = "1.95.0"
components = ["clippy", "rustfmt"]
profile = "minimal"