From 737568718843c2d378d9af2d50907e4f038db5d4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 05:21:13 +0200 Subject: [PATCH 1/4] Pin Rust 1.95.0 and clear the twelve clippy lints red at main rust-toolchain.toml owns which rustc and clippy judge the tree. View in HuginnMindResponse is boxed so the large_enum_variant allow on the wire type is deleted; the PipelineDocument allow keeps a sharpened reason. Type aliases, is_multiple_of, slice::from_ref and a literal refused() clear the rest. Wire bytes and schemas are unchanged. Co-Authored-By: Claude Opus 5.5 --- crates/eureka-pipeline/src/lib.rs | 4 ++-- crates/huginn-daemon/src/daemon.rs | 8 +++++--- crates/huginn-daemon/src/index/mod.rs | 2 +- crates/huginn-daemon/src/index/tests.rs | 9 ++++++--- crates/huginn-daemon/src/serve.rs | 2 +- crates/huginn-mind/src/admission.rs | 6 +++--- crates/huginn-mind/src/wire.rs | 3 +-- crates/mind-body/tests/control.rs | 4 +++- rust-toolchain.toml | 4 ++++ 9 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/crates/eureka-pipeline/src/lib.rs b/crates/eureka-pipeline/src/lib.rs index 3186941..07c6857 100644 --- a/crates/eureka-pipeline/src/lib.rs +++ b/crates/eureka-pipeline/src/lib.rs @@ -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"); diff --git a/crates/huginn-daemon/src/daemon.rs b/crates/huginn-daemon/src/daemon.rs index 614129e..c5453ff 100644 --- a/crates/huginn-daemon/src/daemon.rs +++ b/crates/huginn-daemon/src/daemon.rs @@ -150,7 +150,7 @@ impl> Daemon { 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::Query { selection, semantic: None, .. } => match self.mind.query(&selection) { @@ -495,9 +495,11 @@ pub(crate) mod tests { Ok(Daemon::new(Mind::open(state_root, instance)?, NoIndex)) } + type Seen = Vec<(Vec, Vec)>; + #[derive(Clone, Default)] struct RecordingIndex { - seen: Arc, Vec)>>>, + seen: Arc>, } impl IndexSink for RecordingIndex { @@ -607,7 +609,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 diff --git a/crates/huginn-daemon/src/index/mod.rs b/crates/huginn-daemon/src/index/mod.rs index fef9ebb..d52f5b6 100644 --- a/crates/huginn-daemon/src/index/mod.rs +++ b/crates/huginn-daemon/src/index/mod.rs @@ -596,7 +596,7 @@ impl Projector { 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), } diff --git a/crates/huginn-daemon/src/index/tests.rs b/crates/huginn-daemon/src/index/tests.rs index d18f25b..2e53a16 100644 --- a/crates/huginn-daemon/src/index/tests.rs +++ b/crates/huginn-daemon/src/index/tests.rs @@ -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; +type Edit = (&'static str, Box); + 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>> = const { std::cell::RefCell::new(None) }; + static AFTER_ABSORB: std::cell::RefCell> = 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); 3] = [ + let changes: [Edit; 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); 3] = [ + let cases: [Edit; 3] = [ ( "another mind's label", Box::new(|stored| { diff --git a/crates/huginn-daemon/src/serve.rs b/crates/huginn-daemon/src/serve.rs index 561ad1b..60755d5 100644 --- a/crates/huginn-daemon/src/serve.rs +++ b/crates/huginn-daemon/src/serve.rs @@ -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); diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 5149f77..c05aaff 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -1669,7 +1669,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())])); @@ -1712,7 +1712,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!( @@ -2212,7 +2212,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"), diff --git a/crates/huginn-mind/src/wire.rs b/crates/huginn-mind/src/wire.rs index 5265c0d..6a4cd09 100644 --- a/crates/huginn-mind/src/wire.rs +++ b/crates/huginn-mind/src/wire.rs @@ -102,11 +102,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), + View(Option>), Query(PipelineSelectionPage), Refused(MindRefusal), /// The answer did not fit one send and is being held for the client to diff --git a/crates/mind-body/tests/control.rs b/crates/mind-body/tests/control.rs index 8ab013e..a11068f 100644 --- a/crates/mind-body/tests/control.rs +++ b/crates/mind-body/tests/control.rs @@ -485,13 +485,15 @@ mod unsafe_installs { error.downcast::().expect("the refusal is the typed UnsafeStore error") } + type Case = (&'static str, u32, Option, Option, &'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, Option, &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"), diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..8934eb4 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.95.0" +components = ["clippy", "rustfmt"] +profile = "minimal" From 0c6f6cbb76ac552eb105320e0fe2080c71c8ecd5 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 05:29:03 +0200 Subject: [PATCH 2/4] Map: a campaign's repo set lives on its target (cut-campaign-repos) Step-0b table, authority map and rejected options for how a campaign gains or loses repos, from the Imagination pass that admitted cut-campaign-repos and cut-skill-campaign-repos. Co-Authored-By: Claude Opus 5.5 --- docs/eureka-substrate-map.md | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/eureka-substrate-map.md b/docs/eureka-substrate-map.md index 982a6fe..911f452 100644 --- a/docs/eureka-substrate-map.md +++ b/docs/eureka-substrate-map.md @@ -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 +`: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[16] = absent` on `:target:r`. 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` with the new whole list and the `Superseded` resolution of `r`, 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: }`. | 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 (`:campaign_repo:.n`, 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 (`:campaign:r`): re-keys every stored campaign or adds a second key grammar for one kind, and every `source` citing `: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. From 4d02c50bc20e0e17675819d7b3bb8bbebebd9dcd Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 10 Oct 2026 05:34:58 +0200 Subject: [PATCH 3/4] Target: prose knowledge made accessible to the mind Model page per class of prose knowledge, the one earned kind (observation), voidbot's index as the one retrieval path for prose, sequencing, and the triage of the census's 49 unmapped items (eureka-body:follow_up: prose-knowledge-inventory, ruling prose-knowledge-into-mind). Co-Authored-By: Claude Opus 5.5 --- docs/prose-knowledge-target.md | 470 +++++++++++++++++++++++++++++++++ 1 file changed, 470 insertions(+) create mode 100644 docs/prose-knowledge-target.md diff --git a/docs/prose-knowledge-target.md b/docs/prose-knowledge-target.md new file mode 100644 index 0000000..17d2f58 --- /dev/null +++ b/docs/prose-knowledge-target.md @@ -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 | `:ruling: