Merge origin/main (f003801) into huginn-persona-state
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
e95e2d57aa
12 changed files with 610 additions and 17 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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>),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
470
docs/prose-knowledge-target.md
Normal file
470
docs/prose-knowledge-target.md
Normal 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
4
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "1.95.0"
|
||||
components = ["clippy", "rustfmt"]
|
||||
profile = "minimal"
|
||||
Loading…
Reference in a new issue