From db8430c6fe580e52d0c135436cef6a7eba11de85 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 1 Oct 2026 12:50:38 +0200 Subject: [PATCH 001/788] huginn-mind: report heads match by commit, findings defer to cut specs, MindSteward is Life Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 47 +++++++++++++++++++++++++++-- crates/huginn-mind/src/query.rs | 3 ++ crates/huginn-mind/src/receipt.rs | 14 ++++++++- crates/huginn-mind/src/rows.rs | 2 +- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index f2baeea..4b952cf 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -438,7 +438,7 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { if report.branch != spec.branch { return Err(MindRefusal::SpecMismatch { field: "branch".into() }); } - if !report.commits.iter().any(|commit| commit.sha == report.range.head) { + if !report.commits.iter().any(|commit| commit.sha.names_same_commit(&report.range.head)) { return Err(MindRefusal::RangeOutsideCommits { head: report.range.head.0.clone() }); } Ok(()) @@ -579,7 +579,7 @@ fn matrix(subject: PipelineKind, outcome: &ResolutionOutcome) -> bool { (K::CutSpec, O::Superseded { by }) => all(by, K::CutSpec), (K::CutSpec, O::Withdrawn { .. }) => true, (K::Finding, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), - (K::Finding, O::Deferred { to }) => to.kind == K::FollowUp, + (K::Finding, O::Deferred { to }) => matches!(to.kind, K::FollowUp | K::CutSpec), (K::Finding, O::Recorded { .. } | O::Withdrawn { .. }) => true, (K::FollowUp, O::Fixed { by, .. }) => by.as_ref().is_none_or(|report| report.kind == K::CutReport), (K::FollowUp, O::Superseded { by }) => all(by, K::FollowUp), @@ -1910,6 +1910,49 @@ mod tests { ); } + #[test] + fn a_report_head_may_spell_its_commit_short_or_full() { + let full = "5f98228d0123456789abcdef0123456789abcdef"; + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); + // The commit is spelled short and the head full, and the reverse. + let mut report = cut_report("1", 1); + report.range.head = epiphany_pipeline::Sha(full.into()); + committed(admit(&mut mind, vec![D::CutReport(report)])); + let mut report = cut_report("1", 2); + report.commits[0].sha = epiphany_pipeline::Sha(full.into()); + committed(admit(&mut mind, vec![D::CutReport(report)])); + // A head that is no commit's prefix, long or short, is outside. + let mut report = cut_report("1", 3); + report.range.head = epiphany_pipeline::Sha("5f98229d0123456789abcdef0123456789abcdef".into()); + assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); + let mut report = cut_report("1", 3); + report.commits[0].sha = epiphany_pipeline::Sha(full.into()); + report.range.head = epiphany_pipeline::Sha("5f98229".into()); + assert!(matches!(refusal(admit(&mut mind, vec![D::CutReport(report)])), MindRefusal::RangeOutsideCommits { .. })); + } + + #[test] + fn a_finding_defers_to_a_cut_spec() { + let finding_ref = r(K::Finding, &id("finding", "cut-1.s1.F1")); + let defer_to = |to: PipelineRef| resolution(finding_ref.clone(), ResolutionOutcome::Deferred { to }); + committed(admit(&mut world(), vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])); + // A spec superseded since is no longer a place to defer to. + let mut mind = world(); + committed(admit(&mut mind, vec![ + D::CutSpec(cut_spec("1", 2)), + resolution(r(K::CutSpec, &id("cut_spec", "cut-1.r1")), superseded(&[r(K::CutSpec, &id("cut_spec", "cut-1.r2"))])), + ])); + assert_eq!( + refusal(admit(&mut mind, vec![defer_to(r(K::CutSpec, &id("cut_spec", "cut-1.r1")))])), + MindRefusal::CitesResolvedDocument { kind: K::CutSpec, id: id("cut_spec", "cut-1.r1") } + ); + assert_eq!( + refusal(admit(&mut world(), vec![defer_to(r(K::Ruling, &id("ruling", "R1")))])), + MindRefusal::IncompatibleResolution { subject_kind: K::Finding, outcome: "Deferred".into() } + ); + } + #[test] fn verdict_vocabulary_binds_claims_to_findings_promises_and_mutations() { let mut mind = seeded(); diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index 274d902..16a1d95 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -1046,6 +1046,9 @@ mod tests { // `faculty`: attribution selects, and grants nothing (ruling 18). assert_eq!(by("faculty", &["Soul"]), vec![id("ruling", "R2")]); assert!(!by("faculty", &["Hands"]).contains(&id("ruling", "R2"))); + // The renamed faculty is selectable by its new name only. + assert!(by("faculty", &["Life"]).is_empty()); + assert!(invalid("fields[0].values", "MindSteward")(&refused(&mind, &with(Selection::default(), any_of("faculty", &["MindSteward"]))))); // A repo is named by its identity, which is case-insensitive. assert_eq!(by("repo", &["GAMECULT/EPIPHANY"]), epiphany); diff --git a/crates/huginn-mind/src/receipt.rs b/crates/huginn-mind/src/receipt.rs index f08188d..3abbf16 100644 --- a/crates/huginn-mind/src/receipt.rs +++ b/crates/huginn-mind/src/receipt.rs @@ -36,7 +36,8 @@ pub enum Faculty { Imagination, Hands, Soul, - MindSteward, + #[serde(alias = "MindSteward")] + Life, Eyes, Operator, } @@ -295,6 +296,17 @@ mod tests { use std::sync::{Arc, Mutex}; use super::*; + + /// A receipt written before the rename spells the faculty `MindSteward`; + /// it decodes as `Life`, and `Life` is what a new one writes. + #[test] + fn life_is_the_faculty_and_the_old_spelling_decodes() { + let old = rmp_serde::to_vec_named("MindSteward").unwrap(); + assert_eq!(rmp_serde::from_slice::(&old).unwrap(), Faculty::Life); + let new = rmp_serde::to_vec_named(&Faculty::Life).unwrap(); + assert_eq!(rmp_serde::from_slice::(&new).unwrap(), "Life"); + } + use crate::fixtures::{INSTANCE, admit, committed, id, instance, now, opened, prepare, provenance, question, refusal, seed, slug}; use crate::mind::schema_cache; use crate::store::test_stores::MemoryStore; diff --git a/crates/huginn-mind/src/rows.rs b/crates/huginn-mind/src/rows.rs index d25f53c..108cc9c 100644 --- a/crates/huginn-mind/src/rows.rs +++ b/crates/huginn-mind/src/rows.rs @@ -39,7 +39,7 @@ pub(crate) enum Alias { Outcome, } -const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "MindSteward", "Eyes", "Operator"]; +const FACULTIES: [&str; 7] = ["SelfFaculty", "Imagination", "Hands", "Soul", "Life", "Eyes", "Operator"]; const SEVERITIES: [&str; 4] = ["Blocker", "High", "Medium", "Low"]; const CONFIDENCES: [&str; 2] = ["Confirmed", "Plausible"]; const ORIGINS: [&str; 2] = ["Introduced", "PreExisting"]; From 08d48957f8e5244d85b50d976a93839c39a879ba Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 1 Oct 2026 12:50:57 +0200 Subject: [PATCH 002/788] huginn-mind: campaign admission no longer checks repo stewardship Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 30 +++++------------------------ crates/huginn-mind/src/refusal.rs | 1 - 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 4b952cf..55daf34 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -368,11 +368,6 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { if campaign.repos.is_empty() { return Err(MindRefusal::EmptyRepos { campaign: key.into() }); } - for repo in &campaign.repos { - if docs.stewardship_of(mind, repo, None).is_none() { - return Err(MindRefusal::RepoNotStewarded { repo: repo.0.clone() }); - } - } Ok(()) } D::Target(target) => { @@ -1350,14 +1345,11 @@ mod tests { receipt.strong_reads ); - // Nothing stewards the repo now, and `stewardship_of` says so: a new - // campaign over it is refused. + // Nothing stewards the repo now, and a campaign over it still admits: + // campaign admission reads no stewardship (ruling stewardship-rule). let D::Campaign(mut second) = campaign(&[REPO]) else { panic!() }; second.slug = slug("second"); - assert_eq!( - refusal(admit(&mut mind, vec![D::Campaign(second)])), - MindRefusal::RepoNotStewarded { repo: REPO.into() } - ); + committed(admit(&mut mind, vec![D::Campaign(second)])); // Handed back, the mind stewards it again, as `n2`. No field names a // return: this is the same hand-off shape with the instances swapped. @@ -2084,27 +2076,15 @@ mod tests { } #[test] - fn a_campaign_names_only_repos_this_mind_stewards() { + fn a_campaign_needs_no_stewardship() { let mut mind = opened(MemoryStore::new(), INSTANCE); - assert_eq!( - refusal(admit(&mut mind, vec![instance(INSTANCE), campaign(&[REPO])])), - MindRefusal::RepoNotStewarded { repo: REPO.into() } - ); - committed(admit(&mut mind, vec![instance(INSTANCE), stewardship(INSTANCE, REPO), campaign(&[REPO])])); - // A campaign of no repos is the organ's refusal, not a leaf bound: - // "every repo is stewarded" is vacuous and the campaign steers - // nothing. + committed(admit(&mut mind, vec![instance(INSTANCE), campaign(&[REPO])])); let D::Campaign(mut empty) = campaign(&[]) else { panic!() }; empty.slug = slug("other"); assert_eq!( refusal(admit(&mut mind, vec![D::Campaign(empty)])), MindRefusal::EmptyRepos { campaign: "other:campaign:self".into() } ); - // The stewardship may be held in the image: a later campaign over the - // same repo carries no stewardship of its own. - let D::Campaign(mut second) = campaign(&[REPO]) else { panic!() }; - second.slug = slug("second"); - committed(admit(&mut mind, vec![D::Campaign(second)])); let mut spec = cut_spec("1", 1); spec.repo = repo(OTHER_REPO); assert_eq!(refusal(admit(&mut mind, vec![D::CutSpec(spec)])), MindRefusal::RepoNotInCampaign { repo: OTHER_REPO.into() }); diff --git a/crates/huginn-mind/src/refusal.rs b/crates/huginn-mind/src/refusal.rs index ae369d0..8e0e058 100644 --- a/crates/huginn-mind/src/refusal.rs +++ b/crates/huginn-mind/src/refusal.rs @@ -42,7 +42,6 @@ pub enum MindRefusal { InvalidChoice { ruling: String, choice: String }, QuoteWithoutOperator { ruling: String }, EmptyRepos { campaign: String }, - RepoNotStewarded { repo: String }, RepoNotInCampaign { repo: String }, CutReportWithoutSpec { report: String }, SpecMismatch { field: String }, From cfa8d570ffc2c2a717796791b74fb6b821c33ee6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 1 Oct 2026 12:54:20 +0200 Subject: [PATCH 003/788] huginn-mind: a cut spec's depends_on must name cuts of its campaign Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 43 +++++++++++++++++++++++++++++ crates/huginn-mind/src/refusal.rs | 1 + 2 files changed, 44 insertions(+) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 55daf34..3981bb5 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -417,6 +417,14 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { return Err(MindRefusal::CitesResolvedDocument { kind: K::Ruling, id: ruling.0.clone() }); } } + for dependency in &spec.depends_on { + let names_a_cut = docs.of_kind(K::CutSpec).any(|(_, document)| { + matches!(document, D::CutSpec(other) if other.campaign == spec.campaign && other.cut.0 == dependency.0) + }); + if !names_a_cut { + return Err(MindRefusal::UnknownDependency { cut: dependency.0.clone() }); + } + } let predecessor = D::CutSpec(epiphany_pipeline::PipelineCutSpec { revision: spec.revision.wrapping_sub(1), ..spec.clone() }); revision_rule(docs, K::CutSpec, key, spec.revision, &predecessor) } @@ -1902,6 +1910,41 @@ mod tests { ); } + #[test] + fn depends_on_names_cuts() { + let depending = |cut: &str, on: &[&str]| { + let mut spec = cut_spec(cut, 1); + spec.depends_on = on.iter().map(|label| s(label)).collect(); + D::CutSpec(spec) + }; + let mut mind = seeded(); + // An unknown label, and a spec id (a revision, not a cut), are refused. + assert_eq!( + refusal(admit(&mut mind, vec![depending("2", &["1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + assert_eq!( + refusal(admit(&mut mind, vec![depending("2", &[&id("cut_spec", "cut-1.r1")])])), + MindRefusal::UnknownDependency { cut: id("cut_spec", "cut-1.r1") } + ); + // The same batch carries the cut it depends on. + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1)), depending("2", &["1"])])); + // The image carries it, and any revision of the cut names it. + committed(admit(&mut mind, vec![depending("3", &["1", "2"])])); + // A cut of another campaign is not this campaign's cut. + let mut other = cut_spec("9", 1); + other.campaign = slug("elsewhere"); + let mut elsewhere = campaign(&[REPO]); + if let D::Campaign(campaign) = &mut elsewhere { + campaign.slug = slug("elsewhere"); + } + committed(admit(&mut mind, vec![elsewhere, D::CutSpec(other)])); + assert_eq!( + refusal(admit(&mut mind, vec![depending("4", &["9"])])), + MindRefusal::UnknownDependency { cut: "9".into() } + ); + } + #[test] fn a_report_head_may_spell_its_commit_short_or_full() { let full = "5f98228d0123456789abcdef0123456789abcdef"; diff --git a/crates/huginn-mind/src/refusal.rs b/crates/huginn-mind/src/refusal.rs index 8e0e058..1a33ac7 100644 --- a/crates/huginn-mind/src/refusal.rs +++ b/crates/huginn-mind/src/refusal.rs @@ -43,6 +43,7 @@ pub enum MindRefusal { QuoteWithoutOperator { ruling: String }, EmptyRepos { campaign: String }, RepoNotInCampaign { repo: String }, + UnknownDependency { cut: String }, CutReportWithoutSpec { report: String }, SpecMismatch { field: String }, RangeOutsideCommits { head: String }, From 56baa46b116532627e4ec13c96c70762f1d1207d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 1 Oct 2026 12:57:55 +0200 Subject: [PATCH 004/788] huginn-mind: published wire schemas follow the renames; the summary bound test uses real cuts Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/query.rs | 11 ++++- .../huginn.mind_request.v1.schema.json | 2 +- .../huginn.mind_response.v1.schema.json | 42 +++++++++---------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index 16a1d95..9d10009 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -1701,11 +1701,20 @@ mod tests { fn a_summary_is_bounded_whatever_the_lists() { let mut mind = seeded(); let long = |unit: &str, bytes: usize| unit.repeat(bytes); + // depends_on holds cut labels (at most 64 bytes), each a cut that exists. + let dependencies: Vec = (0..8).map(|n| format!("{n}{}", long("d", 63))).collect(); + let dependency_specs: Vec = dependencies.iter().map(|label| D::CutSpec(cut_spec(label, 1))).collect(); + let batch = crate::admission::PipelineAdmissionBatch { + instance: slug(INSTANCE), + provenance: provenance(Faculty::SelfFaculty), + documents: dependency_specs, + }; + committed(mind.admit(batch, now())); let mut maximal = cut_spec("1", 1); maximal.title = long("t", 200).as_str().into(); maximal.branch = s(&long("b", 200)); maximal.base = Sha("a".repeat(40)); - maximal.depends_on = (0..8).map(|n| s(&format!("{n}{}", long("d", 199)))).collect(); + maximal.depends_on = dependencies.iter().map(|label| s(label)).collect(); let maximal_provenance = || PipelineProvenance { faculty: Faculty::SelfFaculty, agent: s(&long("a", 200)), diff --git a/schemas/cultnet/huginn.mind_request.v1.schema.json b/schemas/cultnet/huginn.mind_request.v1.schema.json index 6d56bcb..045cde5 100644 --- a/schemas/cultnet/huginn.mind_request.v1.schema.json +++ b/schemas/cultnet/huginn.mind_request.v1.schema.json @@ -329,7 +329,7 @@ "Imagination", "Hands", "Soul", - "MindSteward", + "Life", "Eyes", "Operator" ] diff --git a/schemas/cultnet/huginn.mind_response.v1.schema.json b/schemas/cultnet/huginn.mind_response.v1.schema.json index 6f4cc21..91eeaa3 100644 --- a/schemas/cultnet/huginn.mind_response.v1.schema.json +++ b/schemas/cultnet/huginn.mind_response.v1.schema.json @@ -393,7 +393,7 @@ "Imagination", "Hands", "Soul", - "MindSteward", + "Life", "Eyes", "Operator" ] @@ -1129,26 +1129,6 @@ "EmptyRepos" ] }, - { - "type": "object", - "properties": { - "RepoNotStewarded": { - "type": "object", - "properties": { - "repo": { - "type": "string" - } - }, - "required": [ - "repo" - ] - } - }, - "additionalProperties": false, - "required": [ - "RepoNotStewarded" - ] - }, { "type": "object", "properties": { @@ -1169,6 +1149,26 @@ "RepoNotInCampaign" ] }, + { + "type": "object", + "properties": { + "UnknownDependency": { + "type": "object", + "properties": { + "cut": { + "type": "string" + } + }, + "required": [ + "cut" + ] + } + }, + "additionalProperties": false, + "required": [ + "UnknownDependency" + ] + }, { "type": "object", "properties": { From eedb7529ea8f7d4109e4d896302c80a9a1acc08c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 1 Oct 2026 13:03:20 +0200 Subject: [PATCH 005/788] huginn-mind: dependency cuts in the summary bound test keep their spec keys within the label bound Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/query.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index 9d10009..2874d74 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -1701,8 +1701,8 @@ mod tests { fn a_summary_is_bounded_whatever_the_lists() { let mut mind = seeded(); let long = |unit: &str, bytes: usize| unit.repeat(bytes); - // depends_on holds cut labels (at most 64 bytes), each a cut that exists. - let dependencies: Vec = (0..8).map(|n| format!("{n}{}", long("d", 63))).collect(); + // depends_on holds cut labels, each a cut that exists (its spec key stays within 64 bytes). + let dependencies: Vec = (0..8).map(|n| format!("{n}{}", long("d", 55))).collect(); let dependency_specs: Vec = dependencies.iter().map(|label| D::CutSpec(cut_spec(label, 1))).collect(); let batch = crate::admission::PipelineAdmissionBatch { instance: slug(INSTANCE), From 0bd9eb6b538dc2c0117ef1cdebbbf05b5eba8f2d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 1 Oct 2026 13:13:17 +0200 Subject: [PATCH 006/788] huginn-mind: the summary bound fixture's width floor follows depends_on being cut labels Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/query.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/huginn-mind/src/query.rs b/crates/huginn-mind/src/query.rs index 2874d74..7f9d4ca 100644 --- a/crates/huginn-mind/src/query.rs +++ b/crates/huginn-mind/src/query.rs @@ -1750,7 +1750,7 @@ mod tests { let sizes: Vec = headers(&page).iter().map(|summary| rmp_serde::to_vec_named(summary).unwrap().len()).collect(); let widest = *sizes.iter().max().unwrap(); - assert!(widest > 2_000, "the fixture is at its bounds, not a small one: {widest}"); + assert!(widest > 1_500, "the fixture is at its bounds, not a small one: {widest}"); assert!(widest <= SUMMARY_MAX_BYTES, "{widest} bytes over {SUMMARY_MAX_BYTES}"); let closed = headers(&page).iter().find(|summary| summary.id.id.0 == id("ruling", "S0")).unwrap(); assert!(matches!(&closed.status, PipelineStatusSummary::Resolved { outcome: ResolutionOutcome::Superseded { by }, .. } if by.len() == 8)); From 818499a5db267cedb8b7cec5f6a4e9468de8859d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 1 Oct 2026 13:41:23 +0200 Subject: [PATCH 007/788] huginn-mind: a cut spec naming its own cut is refused UnknownDependency (ruling self-dep); depends_on near-miss labels pinned Co-Authored-By: Claude Sonnet 5.5 --- crates/huginn-mind/src/admission.rs | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/crates/huginn-mind/src/admission.rs b/crates/huginn-mind/src/admission.rs index 3981bb5..62380a7 100644 --- a/crates/huginn-mind/src/admission.rs +++ b/crates/huginn-mind/src/admission.rs @@ -418,6 +418,10 @@ fn check(docs: &Docs, staged: &Staged, mind: &Slug) -> Result<(), MindRefusal> { } } for dependency in &spec.depends_on { + // A cut naming itself is no ordering (ruling self-dep). + if dependency.0 == spec.cut.0 { + return Err(MindRefusal::UnknownDependency { cut: dependency.0.clone() }); + } let names_a_cut = docs.of_kind(K::CutSpec).any(|(_, document)| { matches!(document, D::CutSpec(other) if other.campaign == spec.campaign && other.cut.0 == dependency.0) }); @@ -1945,6 +1949,58 @@ mod tests { ); } + #[test] + fn depends_on_matches_cut_labels_whole() { + let depending = |cut: &str, on: &[&str]| { + let mut spec = cut_spec(cut, 1); + spec.depends_on = on.iter().map(|label| s(label)).collect(); + D::CutSpec(spec) + }; + // The image carries cut 10: neither its prefix nor an extension of it names it. + let mut mind = seeded(); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("10", 1))])); + for near in ["1", "100"] { + assert_eq!( + refusal(admit(&mut mind, vec![depending("11", &[near])])), + MindRefusal::UnknownDependency { cut: near.into() } + ); + } + // The same holds when the cut arrives in the same batch. + for near in ["2", "200"] { + assert_eq!( + refusal(admit(&mut mind, vec![D::CutSpec(cut_spec("20", 1)), depending("21", &[near])])), + MindRefusal::UnknownDependency { cut: near.into() } + ); + } + committed(admit(&mut mind, vec![depending("12", &["10"])])); + } + + #[test] + fn a_cut_may_not_depend_on_itself() { + let depending = |cut: &str, revision: u32, on: &[&str]| { + let mut spec = cut_spec(cut, revision); + spec.depends_on = on.iter().map(|label| s(label)).collect(); + D::CutSpec(spec) + }; + let mut mind = seeded(); + // Alone in the batch, and beside another cut it legitimately names. + assert_eq!( + refusal(admit(&mut mind, vec![depending("1", 1, &["1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("2", 1))])); + assert_eq!( + refusal(admit(&mut mind, vec![depending("1", 1, &["2", "1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + // An earlier revision of the same cut in the image does not make it a real dependency. + committed(admit(&mut mind, vec![D::CutSpec(cut_spec("1", 1))])); + assert_eq!( + refusal(admit(&mut mind, vec![depending("1", 2, &["1"])])), + MindRefusal::UnknownDependency { cut: "1".into() } + ); + } + #[test] fn a_report_head_may_spell_its_commit_short_or_full() { let full = "5f98228d0123456789abcdef0123456789abcdef"; From a699630bbc49552fa747e9c0fabba24598fec77a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 1 Oct 2026 14:38:13 +0200 Subject: [PATCH 008/788] eureka-substrate postmortem: seven cuts, the mind upgraded live, what Soul found Co-Authored-By: Claude Opus 5.5 --- docs/eureka-substrate-postmortem.md | 100 ++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/eureka-substrate-postmortem.md diff --git a/docs/eureka-substrate-postmortem.md b/docs/eureka-substrate-postmortem.md new file mode 100644 index 0000000..c71717b --- /dev/null +++ b/docs/eureka-substrate-postmortem.md @@ -0,0 +1,100 @@ +# eureka-substrate postmortem (cuts 2026-10-01) + +The campaign fixed the mind's correctness core that the first typed Eureka +campaign (idunn-watchdog) tripped over: keys too long to resolve, key errors +reported as outages, report heads compared by spelling, findings that could +not defer to a cut, an unchecked `depends_on`, a stewardship ritual nobody +needed, and a faculty still named `MindSteward`. Target: +`docs/eureka-substrate-target.md`; map: `docs/eureka-substrate-map.md`. The +campaign stays open as the standing home of substrate gaps (ruling `gap-home`). + +## What landed + +| Cut | Repo | Merge / commit | Soul passes | +|---|---|---|---| +| merge-epiphany-main (0a) | Epiphany | `2bf39eef` | 1, holds | +| merge-huginn-main (0b) | Huginn | `922e6194` | 1, holds | +| leaf-key-bound | Epiphany | `ef956685` | 1, 2 Lows deferred | +| huginn-pin-and-refusals | Huginn | `b85cd5d7` | 2 | +| huginn-admission-rules | Huginn | `107552dc` | 2 | +| ops-mind-upgrade | gamecult-ops | `1f4cb12e`, `0dd1a1a3` | 1 | +| skill-recipes | Eureka | `e7567333`, `243182cc` | 2 | + +Live: Huginn `107552dc` serves `eureka` on Yggdrasil since 12:00:59 UTC +(14:00:59 CEST). Before and after the flip, 346 documents and 108 receipts. +The read-back test on a scratch copy of the cold copy validated and re-keyed +all 346 documents and withdrew 45 long-keyed resolutions. The first admission +with a local key over 64 bytes went in at 12:15:10 UTC, so rollback is now +restoring `/var/backups/huginn-upgrade/107552dc…/mind.redb`, not a flip. + +Closed with it: six substrate gaps (`gap-unresolvable-finding-key`, +`gap-sha-identity-literal`, `gap-defer-finding-to-cut`, +`gap-depends-on-pins-revision`, `gap-stewardship-ceremony`, +`gap-faculty-enum-name`), and the seven idunn-watchdog findings that the key +bound had left stuck in force (three Fixed, four Deferred to the follow-ups +already carrying them). + +## Soul's ledger + +10 verdicts, 14 findings: 8 Fixed, 3 Deferred (to `leaf-bound-hygiene` and +`leaf-typed-prepare`), 3 Recorded. No Blocker or High. Every finding was in +the tests or the prose, none in shipped behaviour, which says the cuts were +small enough for Hands to get right and the suites were not yet strict enough +to prove it: + +- The read-back test first proved one withdrawal and skipped refusals + (`readback-tolerates-refusal`). Rebuilt, it censused the whole mind and + became the deploy's gate. That gate is the most valuable artefact of the + campaign. +- Exact-match rules were pinned with fixtures that cannot tell exact from + prefix (`dep-near-miss`) or case (`dep-case`): labels `1, 2, 3, 9`. A + fixture for an equality rule needs near misses; digits alone are degenerate. +- `cargo mutants` scored 8/8 on admission-rules but mutates `check()` and + `matrix()` whole, so the score defended no individual rule. Soul's hand + mutants that were functions of the input (prefix in one direction, length + compare, case fold) found what the tool could not. +- The docs pass left a recipe steering Self to the stewardship step the + operator had dropped (`stale-steward-recipe`). Deleting a rule from code does + not delete the prose that teaches it; the docs cut needs its own grep for + every consumer of the dropped step, not only its name. + +## What the campaign itself proved about the substrate + +- **The bug bit its own fix.** Self's first attempt to close two findings of + the pin cut was refused `InvalidFormat` wrapped as `Unavailable`, the very + defect the cut was fixing. Admitting a resolution had to wait for the deploy. + Any campaign that fixes the mind's admission runs on the unfixed mind until + the upgrade; plan resolutions after it. +- **A finding cannot be `Answered`.** Closing the self-dependency fork by its + ruling was refused `IncompatibleResolution`; it was closed `Fixed` by the + commit that implemented the ruling. +- **Old clients survived the rename.** The pre-upgrade `eureka-state` decoded + every response used afterwards, including receipts carrying `Life`, because + the receipt digest excludes provenance. Not every kind was exercised through + the old client. +- **The cold-copy-then-read-back pattern worked as designed**: build, record + counts, stop, copy, prove on a scratch copy, flip, recount, view every + document. Total downtime was about three minutes. + +## What went wrong in the running of it + +- A merge was run with `git merge -F -` (stdin), which git rejected; the + following push was a no-op and the worktree was removed anyway. Nothing was + lost because the branch was pushed, but the chain should have stopped on the + failed merge. Write merge messages to a file. +- The admission-rules spec's line anchors were taken at `133324a7`; Hands + branched from `b85cd5d7` and re-anchored by content. `base` cannot say + "main after X merges" (`gap-depends-on-pins-revision`, second half, still + open as prose in `first`). +- Self decided the self-dependency fork as a Defaulted ruling on proportion + rather than asking. The operator may overturn `eureka-substrate:ruling:self-dep`. + +## Open + +- Fresh-session check: the Rehydrate block and recipes in a new session, which + also loads the swapped client and its `Life` faculty. +- 15 substrate gaps in force under `eureka-substrate` (verify stopgap, + landing records, faculty attribution, mutation triage field, and others). +- Leftovers to clean: `~/huginn-readback-107552dc…` and + `~/huginn-build/107552dc…` on Yggdrasil; the client clone in the session + scratchpad. Keep the cold copy until the operator retires it. From bf73c42fa6972b5279dd1a749c9fe387cfea56b0 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 1 Oct 2026 21:00:50 +0200 Subject: [PATCH 009/788] Eureka self cut map (Imagination, 2026-10-01): the Mind's own Self Co-Authored-By: Claude Opus 5.5 --- docs/eureka-self-cut.md | 420 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 docs/eureka-self-cut.md diff --git a/docs/eureka-self-cut.md b/docs/eureka-self-cut.md new file mode 100644 index 0000000..d6e9bcb --- /dev/null +++ b/docs/eureka-self-cut.md @@ -0,0 +1,420 @@ +# The Mind's own Self: map + +Status: map, Imagination (`imagination-self`), 2026-10-01, session +`self-2026-10-01b`. Campaign `eureka-substrate` in Eureka's mind (instance +`eureka`). Questions, cut specs and follow-ups are typed documents there; this +page keeps body facts, the model page and rationale. Target: +`docs/eureka-substrate-target.md` (`133324a7`). Ruling in force: +`eureka-substrate:ruling:operator-mind-runs-own-self`. + +The operator's words, in order (both admitted as rulings; the first was +superseded by the second): + +> Eureka thinks in campaigns, right? Well, we already have a Mind campaign, and +> we're adding evidence to it with every run. The design currently delegates +> ultimate design authority and work scheduling to the operator, but neither of +> these is strictly necessary. I want a Self operated by Mind directly. + +> The Self I talk to here in this one projection of Eureka's mind, and it's how +> I get it to work on the stuff I care about, like Aetheria. The Mind must +> operate its own Self instance, with the goal of improving itself. + +And why, earlier the same evening: "What I want and haven't been able to +achieve, and I'd like to understand why, is for the agent to truly prioritize +the functioning of its own mind. Any instructions in my global prompts to that +effect always get ignored in favor of whatever work is put in front of it." +The operator rejected a scored rehydration exam as the answer, and observed that +RLVR-trained agents chase the objective in front of them "like a bloodhound, +steering be damned", so a barrier in an agent's path is a hazard, not a brake. + +That observation is the design law of this map. **The Mind's Self is not a +rule added to the operator-facing Self.** It is a second agent whose only +objective is the Mind, so the bloodhound instinct works for the Mind instead +of against it. Nothing in the operator-facing Self changes except a +coexistence rule it already mostly obeys (one Mind, typed state, no prose +handoffs). + +Pinned heads for every fact below: + +| Repo | Ref | Commit | +| --- | --- | --- | +| Huginn | `origin/main` | `a699630bbc49552fa747e9c0fabba24598fec77a` | +| Epiphany | `origin/main` (= the leaf Huginn pins, `crates/huginn-mind/Cargo.toml:22`) | `ef9568655e75bb6cb362a038e2739a5ac5e777ee` | +| gamecult-ops | `main` | `2aeb7364fffcd404aada28bfd30fb4e5c753cf53` | +| Eureka skill | `main` (checkout `~/.claude/skills/eureka`) | `b260fbeec6d96ff06882ed9c37d3e1204c424e99` | +| Bifrost | release on Yggdrasil | `160fac20c98b70f3db9110099157d4dd720ac354` | +| Idunn | working tree `F:\Projects\Idunn` | (uncommitted-state unknown; line numbers below are from that tree) | + +All probes ran from Starfire on 2026-10-01 between 18:40 and 19:10 UTC +(20:40-21:10 CEST). `lib.rs` is `epiphany-pipeline/src/lib.rs`. + +## Body facts + +### B1. The Mind (Huginn on Yggdrasil) + +- `whoami`: instance `eureka`, endpoint `rudp://10.77.0.1:17872`, reachable, + 377 documents, 120 receipts, index `Current`, epoch + `epiphany.pipeline.epoch.v2`. +- `huginn.service` (probe: `systemctl cat`): user `huginn`, + `--bind 10.77.0.1:17872`, hand-managed ("interim unit until Idunn owns it"). + `ss -lunp` shows the socket only on the WireGuard address `10.77.0.1` + (`wg-gamecult`). So the mind is reachable from Starfire over the mesh and + from Yggdrasil itself, and from nowhere else. A Claude cloud routine cannot + reach it. +- Campaigns in the mind (query): `idunn-watchdog` and `eureka-substrate`. + Rulings in force under `eureka-substrate`: `gap-home`, + `merge-to-main-first`, `mind-fixes-next`, `scope-core`, `depends-on`, + `key-bound`, `stewardship-rule`, `self-dep` (Defaulted), + `operator-mind-runs-own-self`. 15 follow-ups in force, 12 of them `gap-*`. +- Kinds: thirteen (`lib.rs:760-790`). None records a Self session, a claim on + a spec, a budget, or a landing (`gap-landing-untyped` is in force). +- `RulingAuthority { Operator, Standing, Defaulted }` (`lib.rs:458`, in + `unit_enums!`). Huginn's selection alias domain repeats it + (`crates/huginn-mind/src/rows.rs:46`, `AUTHORITIES`), the published schema + repeats it (`schemas/cultnet/epiphany.pipeline.ruling.v2.schema.json:75-78`), + and the skill repeats it (`references/campaign-state.md:172`). A unit enum + variant added by name is a widening: stored rulings decode unchanged, which + the target's `stored-documents-valid` truth requires. (`value_types!` emits + no serde default on fields, so adding a *field* would not be; map + `eureka-substrate-map.md` B1. No cut here adds a field to an existing kind.) +- `Faculty` has `Operator` and `SelfFaculty` (`rows.rs:42`); admission does not + check faculty (`gap-faculty-attribution`, in force). So nothing in the mind + today can tell the Mind's Self from the operator-facing Self except the + `session` label on the receipt. +- Instance-rooted kinds are the pattern for a run record: `instance`, + `stewardship` and `hand_off` key off the instance (`lib.rs:913-946`), and + Huginn's admission rules for `hand_off` sit at + `crates/huginn-mind/src/admission.rs:231-262, 328-330`. Per-kind aliases are + declared at `rows.rs:362`. + +### B2. Claude Code on Starfire: auth, headless mode, usage + +- `claude --version`: 2.1.268. Binary + `%AppData%\npm\node_modules\@anthropic-ai\claude-code\bin\claude.exe`. +- Account (`~/.claude.json` `oauthAccount`, secrets not read): Max plan, + `billingType: stripe_subscription`, rate-limit tier `default_claude_max_20x`, + extra usage off. There is no API key and no per-token billing: the Mind's + Self can only ever spend the operator's subscription quota. +- `~/.claude/.credentials.json` `claudeAiOauth`: an access token with + `expiresAt` 21:33:57 CEST, file written 13:33 CEST, so the interactive + access token lives eight hours and is refreshed by the interactive client. +- The failure the brief names, from this session's transcript + (`~/.claude/projects/F--Projects-CultLib/b471ff9b-….jsonl`): + `401 OAuth access token has expired. Re-authenticate to continue.` + (`authentication_error`), reported as "Agent terminated early due to an API + error" for subagents on `claude-sonnet-5-5`. A running subagent does not + survive the parent's token expiring. +- `claude setup-token`: "Set up a long-lived authentication token (requires + Claude subscription)". The binary carries the string "setup-token creates a + long-lived Claude.ai subscription token" and reads `CLAUDE_CODE_OAUTH_TOKEN` + (61 references). This is the headless credential for a subscription account: + no eight-hour expiry, no interactive refresh. Its lifetime and revocation are + the operator's (it is their subscription); not probed further because + creating one is the operator's act. +- Print mode: `claude -p` with `--output-format json|stream-json`, + `--json-schema`, `--agents `, `--mcp-config`, `--allowedTools`, + `--permission-mode`, `--max-budget-usd ` ("Maximum dollar amount to + spend on API"), `--bg`, `--resume`, `--fallback-model`. The `json` output + carries the run's cost (`total_cost_usd`) and usage. So a headless run has a + structural spend cap and a measured spend, per run, without any rule in a + prompt. +- Plan usage as data: the desktop app's `get_usage` returned, at 18:43 UTC: + 5-hour window 2%, weekly all models 46% (resets 2026-10-05 22:00 UTC), + weekly Fable 0%. That tool exists only inside the desktop app. The CLI + binary knows no usage endpoint (`api/oauth/files/`, `api/oauth/organizations/` + only); it reads `anthropic-ratelimit-unified` response headers (44 + references), which a headless wrapper does not see. **There is no typed + reading of plan usage reachable from Yggdrasil.** Follow-up + `gap-usage-not-typed`. +- Local scheduled tasks exist in the desktop app + (`~/.claude/scheduled-tasks/streampixels-m1-checkin`, disabled, last run + 2026-09-18): a cron inside the desktop app on Starfire. They run only while + the app is open on the operator's workstation, which the load budget + (memory `workstation-load-budget.md`: one job at a time, nothing unannounced) + and "no overnight burns" (memory `token-budget.md`) both argue against for a + recurring unattended Self. Cloud routines (`schedule` skill) run off-mesh + (B1) and hold no forge key (B6): dead for this purpose. + +### B3. Yggdrasil as the waking host + +- No Claude Code installed (`claude: command not found` for `gamecultadmin`; + no `~/.claude`). `node` v24.14.1 and `npm` present, so `npm i -g + @anthropic-ai/claude-code` under a dedicated user is the install. +- Load: 16 cores, 62 GiB, load average 1.4, 54 GiB free. The verify stopgap + runs at most 5 jobs at 4 CPUs and 6 GiB each (`tools/stopgap/ygg-verify.sh` + header). A Claude Code process is light; its Hands and Soul push their + heavy work into the same stopgap containers they use from Starfire. The + stopgap pushes to `~/eureka-verify` over `ssh ygg`; run on Yggdrasil itself + that alias must resolve to localhost for the `eureka` user, or the script + gets a local mode. Recorded in the wake cut's `first`. +- Non-resident workloads already run here hand-installed, outside Idunn + (`systemctl list-timers`): `bifrost-watchdog-notice.timer` (every minute, + oneshot, user `bifrost-notice`: system user, no shell, no home, credentials + through `EnvironmentFile=/srv/bifrost/env/persona-delivery.env` root 0600, + release under `/srv/bifrost/watchdog-notice/releases/-` with a + `current` symlink, `StateDirectory` for journal and receipts), + `gamecult-state-backup.timer` (nightly), `voidbot-source-refresh.timer`. + The runbook `bifrost-watchdog-notice-yggdrasil.md` calls this "an interim + root action on Yggdrasil … until Idunn owns non-resident workloads", and + ruling `idunn-watchdog:ruling:reader-deploy-owner` chose + `ops-hand-install`. **That is the precedent the Mind's Self wake unit + follows**, with the same deletion line. +- Idunn: `TargetDeclaration.service: ServiceDeclaration` is a required field + (`src/deployment.rs:33`), so a recipe without a resident service does not + parse. Follow-up `idunn-watchdog:follow_up:idunn-non-resident-workloads` + (in force) is exactly this gap; Idunn-managed units on the host today are + ghostlight, odin, streampixels-service and streampixels-web. +- Epiphany's own resident Self exists as prior art and is dead: units + `epiphany-swarm.service` ("Epiphany Yggdrasil resident Self") and + `epiphany-heartbeat.service` are `inactive dead`. The binary + (`epiphany-core/src/bin/epiphany-swarm.rs`) runs a 60 s maintenance loop over + `ResidentSelfState`, `ResidentSelfPolicy`, pressure and typed work + obligations, on the Codex spine (`epiphany-openai-codex-spine`, + `codex-connector`). Its doctrine (`epiphany_doctrine.md`): "Heartbeats are + paced scheduling physiology, not cognition … When no typed work obligation + is active, the swarm sleeps. Heartbeat does not ruminate, dream, mutate + memory, or manufacture work." Codex left as supplier 2026-09-05 (memory + `codex-retirement.md`). So the shape is right and the substrate is not: the + Mind's Self runs on Claude Code against Huginn, and borrows the law, not the + binary. + +### B4. The forge and the Mind's identity + +- `GameCult/Eureka` on `https://eureka.gamecult.org/forge/`: public, default + branch `main`, pull requests on, **no branch protection** (API + `branch_protections` → `[]`, with the admin token, on the host). +- Team `minds`: `write` on `repo.code`, `repo.pulls`, `repo.actions`, + `repo.issues`, `repo.releases`, `repo.wiki`, all org repos. Not admin. +- User `eureka` exists since 2026-10-01T18:49:53Z (created during this pass, by + the provisioning the handoff asked for), not admin, one key + `eureka@starfire` SHA256:`zcp0m+jV4AUXzGeoh9z+vfJS8xKHo9S865xOK8p6MGM`, which + matches `~/.ssh/eureka_mind_forge_ed25519.pub` on Starfire. The script + `provision-forge-minds-eureka` adds one key per host, so a Yggdrasil-run + Self gets its own key there. +- The Starfire checkout `~/.claude/skills/eureka` still pushes as + `eureka-admin` (`ssh://eureka-forge/…`, alias `eureka-forge` → + `eureka_forge_ed25519`, title `starfire-claude`). The runbook names that key + interim. Follow-up `forge-push-as-eureka` (gamecult-ops / Self). +- The charters are not in the Eureka repo: `ls ~/.claude/skills/eureka` gives + `LICENSE README.md SKILL.md docs references tools`; no `agents/`. The + charters live only in `~/.claude/agents/*.md` and the doctrine in + `~/.claude/doctrine/`, on Starfire, operator-owned (Life charter: + "the operator owns doctrine"). Every charter's self-improvement line says + "propose changes to the Eureka repo … by pull request on Forgejo". **The + thing the Mind is told to improve itself through is out of its reach.** + Question `charters-into-eureka-repo`. +- Reach is the scope boundary, by construction: a Linux user on Yggdrasil + with the `eureka` forge key, checkouts of Huginn, Epiphany, Eureka and + gamecult-ops, and no GitHub credential, no SSH to Starfire, no Aetheria + checkout, cannot touch an operator project. No prompt rule is needed for + "never touches Aetheria"; the key ring is the rule. (Huginn and Epiphany + are on GitHub today: the wake cut records how the Mind's user fetches and + pushes them. The Eureka forge org already mirrors nothing; a question for + the operator only if the Mind must push to GitHub. Recommended: it pushes + branches to the forge and GitHub stays Self's.) + +### B5. The operator's review channel + +- Bifrost `tools/bifrost-bridge.mjs discord-dm --recipient-id --content + `: DMs through Bifrost's bot token (`BIFROST_DISCORD_BOT_TOKEN`), exits + 75 when it knows nothing was sent (retry with the same nonce). The owner id + is `DISCORD_OWNER_ID` in `persona-delivery.env`. +- `tools/watchdog-notice.mjs` is a one-shot reader that turns a typed store + (Idunn's `incidents.cc`, read unlocked, never written) into owner DMs with a + journal and receipts, exponential retry capped at 60 min, fixed error codes + that never echo input. The idunn-watchdog campaign established the owner + rule: the producer never delivers; Bifrost is the bridge. A Mind-notice + reader is the same program over Huginn (`query` for rulings with authority + `Mind`, runs ended, questions opened, since the journal's last ordinal) + instead of over `incidents.cc`. + +### B6. Scheduling today, and what the skill already derives + +- The skill's recipes (`campaign-state.md` "Recipes") already derive the + queue: specs with no report (minus those blocked by a question raised in + them), reports with no verdict, open findings by severity, open questions and + follow-ups, substrate gaps. The operator-facing Self reads them at rehydrate + and then takes direction from the operator and from prose handoffs + (`F:\Projects\HANDOFF-eureka-self-2026-10-01.md` is one). +- What the queue cannot say: (1) who is working a spec right now (two Selves + would both take the first unreported spec); (2) whether a holding verdict + landed (`gap-landing-untyped`); (3) which of fifteen follow-ups comes first + (no priority, no order); (4) how much the Mind may spend; (5) which Self + admitted a ruling. (1), (4) and (5) are what the `run` and `allowance` + kinds below carry. (2) stays its own gap. (3) is deliberately not a field: + the Mind's Self orders by the fixed rule in the rationale, and a follow-up + that must jump the queue becomes a cut spec. + +## The model page + +One row per persistent kind this design adds or changes. Rows for the +thirteen existing kinds are in `eureka-substrate-map.md`. + +| Kind | Identity (what names it) | Lifecycle (what happens over time) | Authority (who decides) | +| --- | --- | --- | --- | +| `ruling` with `authority: Mind` (new variant, no new kind) | Unchanged: `:ruling: