closing rule: acts without a commit close Recorded, Fixed names the merge or squash commit, the reminder bounds Recorded; each flush names its own reason

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-11-09 10:27:54 +01:00
commit 122a968f30
4 changed files with 35 additions and 13 deletions

View file

@ -214,17 +214,17 @@ The run claims `{id}`, which the mind does not hold: its task is unknown.")),
Ok(task) Ok(task)
} }
/// What the child is told once, when its session must end: the ceiling, the cap or the cache. /// What the child is told once, when its session must end, after the reason the flush names for itself.
pub const FLUSH_MESSAGE: &str = "The context ceiling is reached. Admit to the mind what it lacks, close your run, and end."; 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. /// The flush as written: the one message, and what she does with the briefs she holds.
fn flush_text(why: Why) -> String { fn flush_text(why: Why) -> String {
let cause = match why { let cause = match why {
Why::Ceiling => "", 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::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::Idle => "This session ends because nothing is waiting and its cache would lapse.",
}; };
format!("{FLUSH_MESSAGE}{cause} 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.") 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. /// The session id of a run: UUIDv5 over the run's id. Derived, never stored.

View file

@ -123,11 +123,11 @@ pub fn render(briefs: &[Brief], plan: &str) -> String {
/// How a brief closes: the one owner of what done means for her Self. The start message tells it /// 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. /// 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 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 that commit. 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."; 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. /// The reminder under each told brief: the closing rule in short, for this brief's id.
pub fn resolving_sentence(id: &str) -> String { pub fn resolving_sentence(id: &str) -> String {
format!("Close follow-up {id} by the closing rule: Recorded with your answer, Fixed by its commit on the default branch, Superseded by your own follow-up while it waits on someone else, or Withdrawn with why. An open PR is not done.") 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)] #[cfg(test)]

View file

@ -106,7 +106,7 @@ fn a_tell_names_each_brief_the_plan_and_how_she_resolves_it() {
assert!(text.contains("eureka-body:follow_up:b1") && text.contains("Wire the thing."), "{text}"); assert!(text.contains("eureka-body:follow_up:b1") && text.contains("Wire the thing."), "{text}");
let reminder = resolving_sentence("eureka-body:follow_up:b1"); let reminder = resolving_sentence("eureka-body:follow_up:b1");
assert!(text.contains(&reminder), "{text}"); assert!(text.contains(&reminder), "{text}");
for word in ["eureka-body:follow_up:b1", "Recorded", "Fixed", "default branch", "Superseded", "Withdrawn", "An open PR is not done"] { 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!(reminder.contains(word), "{word}: {reminder}");
} }
assert!(!text.contains(CLOSING_RULE), "the rule is told once, at start: {text}"); assert!(!text.contains(CLOSING_RULE), "the rule is told once, at start: {text}");

View file

@ -237,7 +237,7 @@ fn go(steps: Vec<Step>) -> Ran {
type Seed = Box<dyn Fn(&TestMind)>; type Seed = Box<dyn Fn(&TestMind)>;
fn flushes(ran: &Ran) -> usize { 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 ----------------------------------------------------- // ---- who is carried -----------------------------------------------------
@ -1432,12 +1432,34 @@ fn the_start_message_carries_the_closing_rule_once_and_names_what_closes_a_chang
fn the_flush_keeps_a_brief_whose_change_has_not_landed_open() { fn the_flush_keeps_a_brief_whose_change_has_not_landed_open() {
for why in [Why::Ceiling, Why::Rotation, Why::Idle] { for why in [Why::Ceiling, Why::Rotation, Why::Idle] {
let text = flush_text(why); let text = flush_text(why);
assert!(text.starts_with(FLUSH_MESSAGE), "{text}"); 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("stays open") && text.contains("default branch") && text.contains("closing rule"), "{text}");
assert!(!text.contains("Recorded, your answer"), "{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] #[test]
fn a_brief_open_at_start_is_in_the_start_message_with_how_to_resolve_it_and_is_not_told_again() { 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(); let mind = live_mind();
@ -1560,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 ran = mind_run(&mind, steps);
let written = ran.written(); let written = ran.written();
assert_eq!(written.len(), 2, "{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_eq!(ran.spawns.len(), 1);
assert!(ran.spawns[0].iter().all(|arg| arg != "--settings"), "no hooks on argv: {:?}", ran.spawns[0]); assert!(ran.spawns[0].iter().all(|arg| arg != "--settings"), "no hooks on argv: {:?}", ran.spawns[0]);
} }
@ -1588,7 +1610,7 @@ mod live_session {
let entry: Value = serde_json::from_str(entry).unwrap(); let entry: Value = serde_json::from_str(entry).unwrap();
let at = crate::testkit::at((entry["t"].as_f64().unwrap() * 1000.0) as i64); 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) { 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); let (next, action) = decide(state, fact, at);
state = next; state = next;
trace.push((label.to_string(), action)); trace.push((label.to_string(), action));