mind-body: a brief is done when its change lands, told once as one closing rule #21
5 changed files with 72 additions and 17 deletions
|
|
@ -158,8 +158,9 @@ pub const SESSION_NAMESPACE: Uuid = Uuid::NAMESPACE_OID;
|
|||
/// What the child is told at the start. The contract with the `eureka`
|
||||
/// skill: it names the skill, the instance and the run id.
|
||||
pub fn start_message(instance: &Slug, run: &PipelineRef, plan_quota: &[PlanQuota], now_ms: u64) -> String {
|
||||
let closing_rule = inbox::CLOSING_RULE;
|
||||
format!(
|
||||
"Use the eureka skill. You are Self for the instance `{}`; your run is `{}`. Admit as faculty SelfFaculty, agent {AGENT}.\n\nThis session stays open after you answer: briefs from your Persona arrive while you work and between tasks. When you answer a brief, resolve follow-up <its id> Recorded with your answer as rationale; if you decline it, resolve it Withdrawn with why. Work you take on beyond an answer is a follow-up of your own. What you say when you end a turn reaches your Persona as a reply, and your Persona's answers come as briefs. Briefs reach you one conversation at a time. More briefs of the conversation you are serving may arrive while you work; another conversation's brief is told only once you are idle, with no turn running and no background task of yours still running or waiting for its turn. What you say ending any turn, the turns that report your background tasks included, reaches your Persona in the conversation you are serving, so a long background task keeps other members waiting.\n\n{}",
|
||||
"Use the eureka skill. You are Self for the instance `{}`; your run is `{}`. Admit as faculty SelfFaculty, agent {AGENT}.\n\nThis session stays open after you answer: briefs from your Persona arrive while you work and between tasks. {closing_rule} Work you take on beyond an answer is a follow-up of your own. What you say when you end a turn reaches your Persona as a reply, and your Persona's answers come as briefs. Briefs reach you one conversation at a time. More briefs of the conversation you are serving may arrive while you work; another conversation's brief is told only once you are idle, with no turn running and no background task of yours still running or waiting for its turn. What you say ending any turn, the turns that report your background tasks included, reaches your Persona in the conversation you are serving, so a long background task keeps other members waiting.\n\n{}",
|
||||
instance.0,
|
||||
run.id.0,
|
||||
render_plan(plan_quota, now_ms)
|
||||
|
|
@ -213,17 +214,17 @@ The run claims `{id}`, which the mind does not hold: its task is unknown.")),
|
|||
Ok(task)
|
||||
}
|
||||
|
||||
/// What the child is told once, when its session must end: the ceiling, the cap or the cache.
|
||||
pub const FLUSH_MESSAGE: &str = "The context ceiling is reached. Admit to the mind what it lacks, close your run, and end.";
|
||||
/// What the child is told once, when its session must end, after the reason the flush names for itself.
|
||||
pub const FLUSH_MESSAGE: &str = "Admit to the mind what it lacks, close your run, and end.";
|
||||
|
||||
/// The flush as written: the one message, and what she does with the briefs she holds.
|
||||
fn flush_text(why: Why) -> String {
|
||||
let cause = match why {
|
||||
Why::Ceiling => "",
|
||||
Why::Rotation => " (This process ends soon: the unit's time is nearly up. If your next brief comes within the hour, the next process continues this conversation.)",
|
||||
Why::Idle => " (This session ends because nothing is waiting and its cache would lapse.)",
|
||||
Why::Ceiling => "The context ceiling is reached.",
|
||||
Why::Rotation => "This process ends soon: the unit's time is nearly up. If your next brief comes within the hour, the next process continues this conversation.",
|
||||
Why::Idle => "This session ends because nothing is waiting and its cache would lapse.",
|
||||
};
|
||||
format!("{FLUSH_MESSAGE}{cause} Resolve each brief you answered (Recorded, your answer as rationale); a brief you are still working stays open and the next session continues it.")
|
||||
format!("{cause} {FLUSH_MESSAGE} Close each brief that is done by the closing rule; a brief whose change is not on its default branch stays open, or is superseded by your own follow-up when it waits on someone else, and the next session continues it.")
|
||||
}
|
||||
|
||||
/// The session id of a run: UUIDv5 over the run's id. Derived, never stored.
|
||||
|
|
|
|||
|
|
@ -121,9 +121,13 @@ pub fn render(briefs: &[Brief], plan: &str) -> String {
|
|||
blocks.join("\n\n")
|
||||
}
|
||||
|
||||
/// What her Self does when she answers or declines a brief.
|
||||
/// How a brief closes: the one owner of what done means for her Self. The start message tells it
|
||||
/// once; every told brief and the flush point back to it.
|
||||
pub(crate) const CLOSING_RULE: &str = "A brief is done when what it asks for exists. A brief that asks for an answer, a review or a read is done when you answer it: resolve it Recorded with the answer as rationale. A brief that asks for an act with no commit (admitting records, a forge setting, a post, an ops step) is done when the act is done: resolve it Recorded with what you did and where it can be seen. A brief that asks for a change is done when the change is on its repo's default branch, and live where the brief asks for a deploy: resolve it Fixed with the commit on the default branch that contains the change, the merge or squash commit and not the PR head. An open PR, a pushed branch or a local commit is not done, and the brief stays open. In a repo you steward the merge is yours and needs no one's leave: have your Soul check the work, then merge it, then resolve the brief (rulings steward-is-maintainer and merge-gate-own-soul). Land what you have open before you start more. When what is left waits on someone else (a review in a repo you do not steward, a ruling, a deploy you do not hold), admit a follow-up of your own whose item names the PR or branch and whom it waits on, resolve the brief Superseded by it, and say so in your reply. If you decline a brief, resolve it Withdrawn with why.";
|
||||
|
||||
/// The reminder under each told brief: the closing rule in short, for this brief's id.
|
||||
pub fn resolving_sentence(id: &str) -> String {
|
||||
format!("When you answer this brief, resolve follow-up {id} Recorded with your answer as rationale; if you decline it, resolve it Withdrawn with why.")
|
||||
format!("Close follow-up {id} by the closing rule: Recorded only if it asked just for an answer or for an act with no commit, with what you did; Fixed by the merge or squash commit on the default branch if it asked for a change; Superseded by your own follow-up while it waits on someone else; Withdrawn with why. An open PR, a pushed branch or a local commit is not done.")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -104,7 +104,12 @@ fn a_tell_names_each_brief_the_plan_and_how_she_resolves_it() {
|
|||
let brief = Brief { id: "eureka-body:follow_up:b1".into(), campaign: "eureka-body".into(), repo: "GameCult/Huginn".into(), item: "Wire the thing.".into(), ordinal: 1, conversation: persona::Conversation::Public };
|
||||
let text = render(&[brief], "Plan: 5h at 40%.");
|
||||
assert!(text.contains("eureka-body:follow_up:b1") && text.contains("Wire the thing."), "{text}");
|
||||
assert!(text.contains("resolve follow-up eureka-body:follow_up:b1 Recorded") && text.contains("Withdrawn"), "she is told to resolve it: {text}");
|
||||
let reminder = resolving_sentence("eureka-body:follow_up:b1");
|
||||
assert!(text.contains(&reminder), "{text}");
|
||||
for word in ["eureka-body:follow_up:b1", "Recorded", "Fixed", "default branch", "Superseded", "Withdrawn", "An open PR, a pushed branch or a local commit is not done"] {
|
||||
assert!(reminder.contains(word), "{word}: {reminder}");
|
||||
}
|
||||
assert!(!text.contains(CLOSING_RULE), "the rule is told once, at start: {text}");
|
||||
assert!(text.ends_with("Plan: 5h at 40%."), "{text}");
|
||||
assert!(!render(&[], "").contains("brief"), "no brief, no plan, nothing");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ fn go(steps: Vec<Step>) -> Ran {
|
|||
type Seed = Box<dyn Fn(&TestMind)>;
|
||||
|
||||
fn flushes(ran: &Ran) -> usize {
|
||||
ran.written().iter().filter(|text| text.starts_with(FLUSH_MESSAGE)).count()
|
||||
ran.written().iter().filter(|text| text.contains(FLUSH_MESSAGE)).count()
|
||||
}
|
||||
|
||||
// ---- who is carried -----------------------------------------------------
|
||||
|
|
@ -1398,7 +1398,7 @@ fn a_brief_derived_while_a_turn_runs_is_written_at_once_counted_at_its_echo_and_
|
|||
let told = tells(&ran);
|
||||
assert_eq!(told.len(), 1, "{:?}", ran.written());
|
||||
assert!(told[0].contains(&brief_ref("b1").id.0) && told[0].contains("Survey the noise functions."), "{}", told[0]);
|
||||
assert!(told[0].contains("resolve follow-up") && told[0].contains("Recorded"), "she is told how to close it: {}", told[0]);
|
||||
assert!(told[0].contains(&inbox::resolving_sentence(&brief_ref("b1").id.0)) && !told[0].contains(inbox::CLOSING_RULE), "she is reminded of the closing rule, not told it again: {}", told[0]);
|
||||
// The plan line is restated under every brief for the run's life, so it names the instant of the reading, never an age that stops being true.
|
||||
assert!(told[0].contains("read at 2026-10-29T20:33Z") && !told[0].contains(" ago"), "{}", told[0]);
|
||||
assert_eq!(ran.written().len(), 2, "the start message and one tell: {:?}", ran.written());
|
||||
|
|
@ -1419,6 +1419,47 @@ fn a_brief_derived_while_the_session_is_idle_is_written_and_the_session_is_busy_
|
|||
assert!(resolutions_of(&mind, &brief_ref("b2")).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_start_message_carries_the_closing_rule_once_and_names_what_closes_a_change() {
|
||||
let text = start_message(&Slug("yggdrasil".into()), &run_ref("selfrun-a"), &[], 0);
|
||||
assert_eq!(text.matches(inbox::CLOSING_RULE).count(), 1, "{text}");
|
||||
for word in ["default branch", "Recorded", "Fixed", "Superseded", "Withdrawn", "merge-gate-own-soul", "An open PR, a pushed branch or a local commit is not done"] {
|
||||
assert!(text.contains(word), "{word}: {text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_flush_keeps_a_brief_whose_change_has_not_landed_open() {
|
||||
for why in [Why::Ceiling, Why::Rotation, Why::Idle] {
|
||||
let text = flush_text(why);
|
||||
assert!(text.contains(FLUSH_MESSAGE), "{text}");
|
||||
assert_eq!(text.contains("context ceiling"), why == Why::Ceiling, "each flush names its own reason: {text}");
|
||||
assert!(text.contains("stays open") && text.contains("default branch") && text.contains("closing rule"), "{text}");
|
||||
assert!(!text.contains("Recorded, your answer"), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_closing_rule_covers_an_act_without_a_commit_and_names_the_commit_fixed_takes() {
|
||||
let rule = inbox::CLOSING_RULE;
|
||||
for word in ["no commit", "Recorded with what you did and where it can be seen", "merge or squash commit", "not the PR head"] {
|
||||
assert!(rule.contains(word), "{word}: {rule}");
|
||||
}
|
||||
let reminder = inbox::resolving_sentence("x");
|
||||
for word in ["Recorded only if it asked just for an answer", "an act with no commit", "a pushed branch or a local commit is not done"] {
|
||||
assert!(reminder.contains(word), "{word}: {reminder}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_source_of_the_actuator_tells_her_a_brief_closes_by_recorded_with_her_answer() {
|
||||
for source in [include_str!("../self_run.rs"), include_str!("inbox.rs")] {
|
||||
for old in ["Recorded with your answer", "Recorded, your answer", "Resolve each brief you answered"] {
|
||||
assert!(!source.contains(old), "{old}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_brief_open_at_start_is_in_the_start_message_with_how_to_resolve_it_and_is_not_told_again() {
|
||||
let mind = live_mind();
|
||||
|
|
@ -1426,7 +1467,8 @@ fn a_brief_open_at_start_is_in_the_start_message_with_how_to_resolve_it_and_is_n
|
|||
let ran = mind_run(&mind, vec![Out(echo()), Idle(31), Idle(31)]);
|
||||
let start = &ran.written()[0];
|
||||
assert!(start.contains("Also open for you") && start.contains(&brief_ref("p1").id.0) && start.contains("Still open."), "{start}");
|
||||
assert!(start.contains(&format!("resolve follow-up {} Recorded", brief_ref("p1").id.0)), "{start}");
|
||||
assert!(start.contains(&inbox::resolving_sentence(&brief_ref("p1").id.0)), "{start}");
|
||||
assert_eq!(start.matches(inbox::CLOSING_RULE).count(), 1, "the rule is told once, beside the reminder: {start}");
|
||||
assert_eq!(ran.written().len(), 1, "never told again: {:?}", ran.written());
|
||||
}
|
||||
|
||||
|
|
@ -1493,7 +1535,7 @@ fn the_session_ends_where_the_cache_would_be_evicted_with_a_flush_and_a_waiting_
|
|||
let closed_at = hour.events.iter().position(|event| *event == Event::InputClosed).unwrap();
|
||||
assert!(flush_at < closed_at && hour.count(&Event::InputClosed) == 1, "{:?}", hour.events);
|
||||
assert!(hour.out.contains("decision Write { tells: false, switch: false, flush: Some(Idle) } prefix 10 ttl_s 3600 idle_s 3540 inbox false written 2 consumed 1\n"), "{}", hour.out);
|
||||
assert!(hour.written().iter().any(|text| text.contains("nothing is waiting and its cache would lapse") && text.contains("Resolve each brief you answered")), "{:?}", hour.written());
|
||||
assert!(hour.written().iter().any(|text| text.contains("nothing is waiting and its cache would lapse") && text.contains("by the closing rule")), "{:?}", hour.written());
|
||||
// Five minutes' cache: the flush at 240 s.
|
||||
let short = go(vec![Out(echo()), Out(five_minute_request("m1", 10)), Out(ok_result()), Idle(239), Idle(1), Out(echo()), Out(ok_result()), Idle(1)]);
|
||||
assert_eq!(flushes(&short), 1);
|
||||
|
|
@ -1540,7 +1582,7 @@ fn no_stdin_line_is_written_after_the_flush_and_nothing_but_the_writer_writes()
|
|||
let ran = mind_run(&mind, steps);
|
||||
let written = ran.written();
|
||||
assert_eq!(written.len(), 2, "{written:?}");
|
||||
assert!(written[1].starts_with(FLUSH_MESSAGE), "{}", written[1]);
|
||||
assert!(written[1].contains(FLUSH_MESSAGE), "{}", written[1]);
|
||||
assert_eq!(ran.spawns.len(), 1);
|
||||
assert!(ran.spawns[0].iter().all(|arg| arg != "--settings"), "no hooks on argv: {:?}", ran.spawns[0]);
|
||||
}
|
||||
|
|
@ -1568,7 +1610,7 @@ mod live_session {
|
|||
let entry: Value = serde_json::from_str(entry).unwrap();
|
||||
let at = crate::testkit::at((entry["t"].as_f64().unwrap() * 1000.0) as i64);
|
||||
if let Some(text) = entry.get("want").and_then(Value::as_str) {
|
||||
let (label, fact) = if text.starts_with(FLUSH_MESSAGE) { ("flush-asked", Fact::Tick) } else { ("want", Fact::Inbox { pending: true, other: false, other_first: false }) };
|
||||
let (label, fact) = if text.contains(FLUSH_MESSAGE) { ("flush-asked", Fact::Tick) } else { ("want", Fact::Inbox { pending: true, other: false, other_first: false }) };
|
||||
let (next, action) = decide(state, fact, at);
|
||||
state = next;
|
||||
trace.push((label.to_string(), action));
|
||||
|
|
|
|||
|
|
@ -836,7 +836,10 @@ in-force `operated_by Mind` Self run of its instance, which admission keeps uniq
|
|||
echo; the grace interrupt (earlier of 15 min after the ask and the cap less the ladder) is the
|
||||
backstop. A brief is an in-force `self-run` follow-up her Persona turn admitted (the receipt's
|
||||
agent, not the source the writer named); the actuator closes none, and a brief left open is
|
||||
told to the next session. Compaction is
|
||||
told to the next session. Briefs close by
|
||||
`inbox::CLOSING_RULE`, told once at start (Recorded for answers, Fixed by a default-branch commit for
|
||||
changes, Superseded by her own follow-up while a change waits on someone else); each told brief and
|
||||
the flush carry a reminder that points at it. Compaction is
|
||||
off in the CLI (`DISABLE_COMPACT=1` in the child environment).
|
||||
- With `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB` set the CLI forces the permission mode to default:
|
||||
`--permission-mode bypassPermissions` has no effect there and only the seed's allow list decides
|
||||
|
|
|
|||
Loading…
Reference in a new issue