From df987c30583de4bf66c74ac8a3c6499d4241fc7a Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 3 Oct 2026 01:31:07 -0700 Subject: [PATCH] docs: M5d-2 checklist: given-test lifetimes and cron AND session_slot and the scheduler tests held a temporary JSON Value across statements (E0716). Cron now pins DOM and DOW as AND, which the given tests had not. Gate counts from task 04 on move up by one. Co-Authored-By: Grok 4.6 --- docs/plans/M5d2/01-loopd-session-slot.md | 7 ++++--- docs/plans/M5d2/04-loopd-cron.md | 11 ++++++----- docs/plans/M5d2/05-loopd-jobs.md | 6 +++--- docs/plans/M5d2/06-loopd-scheduler-heartbeat.md | 6 +++--- docs/plans/M5d2/07-loopd-scheduler-cron.md | 6 +++--- docs/plans/M5d2/08-loopd-tool-summary.md | 6 +++--- docs/plans/M5d2/09-memoryd-summary-index.md | 6 +++--- docs/plans/M5d2/10-memoryd-tiers.md | 6 +++--- docs/plans/M5d2/README.md | 16 ++++++++-------- docs/plans/M5d2/files/crates/loopd/tests/cron.rs | 9 +++++++++ .../files/crates/loopd/tests/scheduler_cron.rs | 7 ++----- .../crates/loopd/tests/scheduler_heartbeat.rs | 7 ++----- .../files/crates/loopd/tests/session_slot.rs | 2 +- 13 files changed, 50 insertions(+), 45 deletions(-) diff --git a/docs/plans/M5d2/01-loopd-session-slot.md b/docs/plans/M5d2/01-loopd-session-slot.md index 8b5dfd6..107d2a0 100644 --- a/docs/plans/M5d2/01-loopd-session-slot.md +++ b/docs/plans/M5d2/01-loopd-session-slot.md @@ -80,10 +80,11 @@ become Then `git status --short` lists one new file, `crates/loopd/tests/session_slot.rs`. - [ ] **2. See it fail.** `cargo test -p loopd --test session_slot`, with a shell timeout of - 600000 ms: the four tests compile (they call `session.slot()`, which exists) and + 600000 ms: the four tests compile (they call `session.slot()`, which exists) and three fail. `a_session_created_on_the_background_slot_sends_that_slot` fails: `id_slot` is `0`, not `1`. - `a_main_slot_session_still_sends_main` may pass. `the_request_slot_follows_session_start_not_the_config_main` - fails: `id_slot` is `0`, not `7`. + `a_main_slot_session_still_sends_main` passes. `the_request_slot_follows_session_start_not_the_config_main` + fails: `id_slot` is `0`, not `7`. `a_heartbeat_turn_between_two_main_turns_does_not_change_the_main_prefix` + fails: the heartbeat request's `id_slot` is `0`, not `1`. - [ ] **3. The edit.** Make edit T1. `cargo check -p loopd` prints no error and no warning. - [ ] **4. The tests.** `cargo test -p loopd --test session_slot --test turn --test channel --test compact_turn`: every test passes, 4 in `session_slot` and the others as before. diff --git a/docs/plans/M5d2/04-loopd-cron.md b/docs/plans/M5d2/04-loopd-cron.md index 1ba41bb..42d4ee4 100644 --- a/docs/plans/M5d2/04-loopd-cron.md +++ b/docs/plans/M5d2/04-loopd-cron.md @@ -165,28 +165,29 @@ If `is_multiple_of` is not in scope on `u32` in this toolchain, use `value % n = target compiles. - [ ] **3. The module.** Write `cron.rs` from the skeleton. Add `pub mod cron;` to `lib.rs` where rustfmt will put it. `cargo check -p loopd` prints no error and no warning. -- [ ] **4. The tests.** `cargo test -p loopd --test cron`: every test passes, 10 tests. +- [ ] **4. The tests.** `cargo test -p loopd --test cron`: every test passes, 11 tests. - [ ] **5. Check.** `git diff --stat` lists `crates/loopd/src/cron.rs` and `crates/loopd/src/lib.rs`. - [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line - `gate: ok`. Test count **1536** (1526 before, plus 10). + `gate: ok`. Test count **1537** (1526 before, plus 11). - [ ] **7. Log and commit.** Task cell `M5d2/04-loopd-cron`. Stage with `git add crates/loopd docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. ## Done when -- `make gate` prints `gate: ok` with 1536 tests. +- `make gate` prints `gate: ok` with 1537 tests. - `git diff HEAD~1 --stat` lists only `crates/loopd/src/cron.rs`, `crates/loopd/src/lib.rs`, `crates/loopd/tests/cron.rs` and the log. ## Stop and report if - The branch check fails; step 2 differs; `cargo check` warns; a test fails; the gate count is - not 1536; you add a dependency; you treat DOM and DOW as OR; you accept `@daily` or a sixth + not 1537; you add a dependency; you treat DOM and DOW as OR; you accept `@daily` or a sixth field; `cron.rs` exceeds 500 lines. ## What the given tests pin `0 7 * * *` matches that minute only; `* * * * *` matches; `*/5` is multiples from 0; `0-10` -is inclusive; Sunday is 0 on 2026-10-04; the 1st of the month; four or six fields error; +is inclusive; Sunday is 0 on 2026-10-04; the 1st of the month; DOM and DOW are AND +(`0 7 1 * 0` matches 2026-11-01, not 2026-10-04 or 2026-10-01); four or six fields error; minute 60, hour 24, DOW 7 error; `@daily` errors; `Display` names the schedule. diff --git a/docs/plans/M5d2/05-loopd-jobs.md b/docs/plans/M5d2/05-loopd-jobs.md index ba61c94..d14c601 100644 --- a/docs/plans/M5d2/05-loopd-jobs.md +++ b/docs/plans/M5d2/05-loopd-jobs.md @@ -194,21 +194,21 @@ Replace the four stub bodies in the same task. Do not leave a stub that returns - [ ] **6. Check.** `git diff --stat` lists `crates/loopd/src/jobs.rs`, `crates/loopd/src/lib.rs`, `crates/loopd/src/main.rs`, `docs/runbook.md`. - [ ] **7. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line - `gate: ok`. Test count **1544** (1536 before, plus 8). + `gate: ok`. Test count **1545** (1537 before, plus 8). - [ ] **8. Log and commit.** Task cell `M5d2/05-loopd-jobs`. Stage with `git add crates/loopd docs/runbook.md docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. ## Done when -- `make gate` prints `gate: ok` with 1544 tests. +- `make gate` prints `gate: ok` with 1545 tests. - `git diff HEAD~1 --stat` lists only `crates/loopd/src/jobs.rs`, `crates/loopd/src/lib.rs`, `crates/loopd/src/main.rs`, `crates/loopd/tests/jobs.rs`, `docs/runbook.md` and the log. ## Stop and report if - The branch check fails; step 2 differs; `cargo check` warns; a test fails; the gate count is - not 1544; you add a cron crate; a missing jobs directory is an error; `Display` for `Invalid` + not 1545; you add a cron crate; a missing jobs directory is an error; `Display` for `Invalid` does not end with the runbook pointer; `jobs.rs` exceeds 500 lines; you write a job file. ## What the given tests pin diff --git a/docs/plans/M5d2/06-loopd-scheduler-heartbeat.md b/docs/plans/M5d2/06-loopd-scheduler-heartbeat.md index da0e76f..4012084 100644 --- a/docs/plans/M5d2/06-loopd-scheduler-heartbeat.md +++ b/docs/plans/M5d2/06-loopd-scheduler-heartbeat.md @@ -190,14 +190,14 @@ pass so it runs once. `grep -c '^## heartbeat-prompt-unreadable$' docs/runbook.md` prints `1`. - [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line - `gate: ok`. Test count **1550** (1544 before, plus 6). + `gate: ok`. Test count **1551** (1545 before, plus 6). - [ ] **7. Log and commit.** Task cell `M5d2/06-loopd-scheduler-heartbeat`. Stage with `git add crates/loopd docs/runbook.md docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. ## Done when -- `make gate` prints `gate: ok` with 1550 tests. +- `make gate` prints `gate: ok` with 1551 tests. - `git diff HEAD~1 --stat` lists only `crates/loopd/src/channel.rs`, `crates/loopd/src/scheduler.rs`, `crates/loopd/src/lib.rs`, `crates/loopd/src/main.rs`, `crates/loopd/tests/scheduler_heartbeat.rs`, `docs/runbook.md` and the log. @@ -205,7 +205,7 @@ pass so it runs once. ## Stop and report if - The branch check fails; a "before" block is not found exactly once; step 2 differs; `cargo - check` warns; a test fails; the gate count is not 1550; you sleep in a test; you post to + check` warns; a test fails; the gate count is not 1551; you sleep in a test; you post to Mattermost; you send `slots.main`; you set a speaker; you `chmod 000`; `Held` stays private. ## What the given tests pin diff --git a/docs/plans/M5d2/07-loopd-scheduler-cron.md b/docs/plans/M5d2/07-loopd-scheduler-cron.md index c8d90a0..8d6e297 100644 --- a/docs/plans/M5d2/07-loopd-scheduler-cron.md +++ b/docs/plans/M5d2/07-loopd-scheduler-cron.md @@ -82,20 +82,20 @@ Do not change heartbeat behaviour. Do not add a sleep. --test jobs --test cron`: every test passes, 6 in `scheduler_cron`. - [ ] **5. Check.** `git diff --stat` lists exactly `crates/loopd/src/scheduler.rs`. - [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line - `gate: ok`. Test count **1556** (1550 before, plus 6). + `gate: ok`. Test count **1557** (1551 before, plus 6). - [ ] **7. Log and commit.** Task cell `M5d2/07-loopd-scheduler-cron`. Stage with `git add crates/loopd docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. ## Done when -- `make gate` prints `gate: ok` with 1556 tests. +- `make gate` prints `gate: ok` with 1557 tests. - `git diff HEAD~1 --stat` lists only `crates/loopd/src/scheduler.rs`, `crates/loopd/tests/scheduler_cron.rs` and the log. ## Stop and report if - The branch check fails; step 2 differs (tests do not compile, or a different assertion - fails); `cargo check` warns; a test fails; the gate count is not 1556; you reuse a session + fails); `cargo check` warns; a test fails; the gate count is not 1557; you reuse a session across runs; you send `slots.main`; you set a speaker; you skip leftovers that are not busy; a heartbeat test fails. diff --git a/docs/plans/M5d2/08-loopd-tool-summary.md b/docs/plans/M5d2/08-loopd-tool-summary.md index 398dd74..19959ae 100644 --- a/docs/plans/M5d2/08-loopd-tool-summary.md +++ b/docs/plans/M5d2/08-loopd-tool-summary.md @@ -231,14 +231,14 @@ Facts). The `Err(TurnError::SessionFull)` arm gets the same `missing` call befor - [ ] **5. Check.** `git diff --stat` lists `crates/loopd/src/summarise.rs`, `crates/loopd/src/lib.rs`, `crates/loopd/src/compact.rs`. - [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line - `gate: ok`. Test count **1562** (1556 before, plus 6). + `gate: ok`. Test count **1563** (1557 before, plus 6). - [ ] **7. Log and commit.** Task cell `M5d2/08-loopd-tool-summary`. Stage with `git add crates/loopd config/summarise.md docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. ## Done when -- `make gate` prints `gate: ok` with 1562 tests. +- `make gate` prints `gate: ok` with 1563 tests. - `git diff HEAD~1 --stat` lists only `crates/loopd/src/summarise.rs`, `crates/loopd/src/lib.rs`, `crates/loopd/src/compact.rs`, `crates/loopd/tests/tool_summary_call.rs`, `config/summarise.md` and the log. @@ -246,7 +246,7 @@ Facts). The `Err(TurnError::SessionFull)` arm gets the same `missing` call befor ## Stop and report if - The branch check fails; a "before" block is not found exactly once; step 2 differs; `cargo - check` warns; a test fails; the gate count is not 1562; you send tools on the summary + check` warns; a test fails; the gate count is not 1563; you send tools on the summary request; you send `slots.main`; you replay the summary in `messages()`; you summarise a name not in the list; you summarise twice; you compact before summarising. diff --git a/docs/plans/M5d2/09-memoryd-summary-index.md b/docs/plans/M5d2/09-memoryd-summary-index.md index a5e7e33..890897f 100644 --- a/docs/plans/M5d2/09-memoryd-summary-index.md +++ b/docs/plans/M5d2/09-memoryd-summary-index.md @@ -117,21 +117,21 @@ results are indexed. A comment that does not fit its code: stop. test passes, 5 in `tool_summary_index`. - [ ] **5. Check.** `git diff --stat` lists exactly `crates/memoryd/src/index.rs`. - [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line - `gate: ok`. Test count **1567** (1562 before, plus 5). + `gate: ok`. Test count **1568** (1563 before, plus 5). - [ ] **7. Log and commit.** Task cell `M5d2/09-memoryd-summary-index`. Stage with `git add crates/memoryd docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. ## Done when -- `make gate` prints `gate: ok` with 1567 tests. +- `make gate` prints `gate: ok` with 1568 tests. - `git diff HEAD~1 --stat` lists only `crates/memoryd/src/index.rs`, `crates/memoryd/tests/tool_summary_index.rs` and the log. ## Stop and report if - The branch check fails; a "before" block is not found exactly once; step 2 differs; `cargo - check` warns; a test fails; the gate count is not 1567; you index `ToolResult` content; you + check` warns; a test fails; the gate count is not 1568; you index `ToolResult` content; you leave `kind` as `"session"`; a secret summary is searchable on a private index. ## What the given tests pin diff --git a/docs/plans/M5d2/10-memoryd-tiers.md b/docs/plans/M5d2/10-memoryd-tiers.md index 1278f62..5af8542 100644 --- a/docs/plans/M5d2/10-memoryd-tiers.md +++ b/docs/plans/M5d2/10-memoryd-tiers.md @@ -203,20 +203,20 @@ becomes - [ ] **5. Check.** `git diff --stat` lists the files in "Every place that changes" except the new tests and starter (those are untracked until `git add`). - [ ] **6. Run the gate.** `cargo fmt --all`, then `make gate`, timeout 600000 ms. Last line - `gate: ok`. Test count **1573** (1567 before, plus 6). + `gate: ok`. Test count **1574** (1568 before, plus 6). - [ ] **7. Log and commit.** Task cell `M5d2/10-memoryd-tiers`. Stage with `git add crates/bxcore crates/memoryd crates/bxctl config/jobs docs/implementer-log.md Cargo.lock`, then commit as `AGENTS.md` says. ## Done when -- `make gate` prints `gate: ok` with 1573 tests. +- `make gate` prints `gate: ok` with 1574 tests. - `git diff HEAD~1 --stat` lists only the files this task names plus the log. ## Stop and report if - The branch check fails; a "before" block is not found exactly once; step 2 differs; `cargo - check` warns; a test fails; the gate count is not 1573; you edit `cli_memory.rs`; identity + check` warns; a test fails; the gate count is not 1574; you edit `cli_memory.rs`; identity entries show a non-zero hit count; untrusted notes are omitted; you add a cron crate; you special-case the name `tier` in `loopd`. diff --git a/docs/plans/M5d2/README.md b/docs/plans/M5d2/README.md index 56a41d4..4722590 100644 --- a/docs/plans/M5d2/README.md +++ b/docs/plans/M5d2/README.md @@ -63,13 +63,13 @@ its own. Before task 01 it is **1508** (straylight, rustc 1.98.1 / clippy 0.1.98 | 01 | `run_turn` sends `session.slot()`; a session created on the background slot has `id_slot` of that slot; `mm-*` stays main | loopd | 1512 | 4 | | 02 | `LogRecord::ToolSummary`; `messages()` ignores it; `types` names it; JSON round-trip | bxcore, loopd | 1518 | 6 | | 03 | `[heartbeat]` (`every_s` 0, `prompt` `heartbeat.md`); unknown `[compact] summarise` names are a config error; starter `heartbeat.md` | loopd | 1526 | 8 | -| 04 | Five-field UTC cron matcher: `*`, `n`, `n-m`, `*/n`; day-of-week 0 is Sunday | loopd | 1536 | 10 | -| 05 | `{home}/jobs/.toml`, `runs.jsonl`, `jobs-invalid` pointer and runbook | loopd | 1544 | 8 | -| 06 | Heartbeat tick: time line, standing session `heartbeat`, skip if busy, unreadable prompt logged once | loopd | 1550 | 6 | -| 07 | Cron tick: new session per run, skip if previous live, leftover `started` becomes `failed` | loopd | 1556 | 6 | -| 08 | After a turn, opted-in `ToolResult` without a `ToolSummary` gets a background summary appended with the result's labels | loopd | 1562 | 6 | -| 09 | `memoryd` indexes `ToolSummary` as kind `summary`; `recall` finds it and taints when untrusted | memoryd | 1567 | 5 | -| 10 | `MemoryCommand::Tiers`, `bxctl memory tiers`, starter `config/jobs/tier.toml.example` | bxcore, memoryd, bxctl | 1573 | 6 | +| 04 | Five-field UTC cron matcher: `*`, `n`, `n-m`, `*/n`; day-of-week 0 is Sunday | loopd | 1537 | 11 | +| 05 | `{home}/jobs/.toml`, `runs.jsonl`, `jobs-invalid` pointer and runbook | loopd | 1545 | 8 | +| 06 | Heartbeat tick: time line, standing session `heartbeat`, skip if busy, unreadable prompt logged once | loopd | 1551 | 6 | +| 07 | Cron tick: new session per run, skip if previous live, leftover `started` becomes `failed` | loopd | 1557 | 6 | +| 08 | After a turn, opted-in `ToolResult` without a `ToolSummary` gets a background summary appended with the result's labels | loopd | 1563 | 6 | +| 09 | `memoryd` indexes `ToolSummary` as kind `summary`; `recall` finds it and taints when untrusted | memoryd | 1568 | 5 | +| 10 | `MemoryCommand::Tiers`, `bxctl memory tiers`, starter `config/jobs/tier.toml.example` | bxcore, memoryd, bxctl | 1574 | 6 | After task 10, the design model runs the M5d-2 device check (spec 7) on straylight: a heartbeat turn on the background slot leaves a following main-slot turn's `cache_n` near its previous @@ -129,4 +129,4 @@ OPENCODE_MODEL=llama.cpp/ornith-1.5-35b-a3b RESUME=1 \ tools/run-plan.sh docs/plans/M5d2 01 ``` -After task 10, the gate count is 1573. Then the device check above. +After task 10, the gate count is 1574. Then the device check above. diff --git a/docs/plans/M5d2/files/crates/loopd/tests/cron.rs b/docs/plans/M5d2/files/crates/loopd/tests/cron.rs index 0aa422b..6a4c964 100644 --- a/docs/plans/M5d2/files/crates/loopd/tests/cron.rs +++ b/docs/plans/M5d2/files/crates/loopd/tests/cron.rs @@ -52,6 +52,15 @@ fn the_first_of_the_month() { assert!(!due("0 7 1 * *", "2026-10-02T07:00:00.000Z")); } +#[test] +fn day_of_month_and_day_of_week_are_both_required() { + // 2026-10-04 is a Sunday, not the 1st. 2026-10-01 is a Thursday. + assert!(!due("0 7 1 * 0", "2026-10-04T07:00:00.000Z")); + assert!(!due("0 7 1 * 0", "2026-10-01T07:00:00.000Z")); + // 2026-11-01 is a Sunday and the 1st. + assert!(due("0 7 1 * 0", "2026-11-01T07:00:00.000Z")); +} + #[test] fn four_or_six_fields_are_errors() { assert!(matches!( diff --git a/docs/plans/M5d2/files/crates/loopd/tests/scheduler_cron.rs b/docs/plans/M5d2/files/crates/loopd/tests/scheduler_cron.rs index bc21e08..c4356f7 100644 --- a/docs/plans/M5d2/files/crates/loopd/tests/scheduler_cron.rs +++ b/docs/plans/M5d2/files/crates/loopd/tests/scheduler_cron.rs @@ -64,11 +64,8 @@ fn a_due_job_starts_a_new_session_on_the_background_slot() { let sent = server.requests_to(CHAT); assert_eq!(sent.len(), 1); assert_eq!(sent[0].json()["id_slot"], 1); - let last = sent[0].json()["messages"] - .as_array() - .unwrap() - .last() - .unwrap(); + let body = sent[0].json(); + let last = body["messages"].as_array().unwrap().last().unwrap(); let content = last["content"].as_str().unwrap(); assert!( content.starts_with("It is 2026-10-03T07:00:00.000Z."), diff --git a/docs/plans/M5d2/files/crates/loopd/tests/scheduler_heartbeat.rs b/docs/plans/M5d2/files/crates/loopd/tests/scheduler_heartbeat.rs index a6b1b46..303250e 100644 --- a/docs/plans/M5d2/files/crates/loopd/tests/scheduler_heartbeat.rs +++ b/docs/plans/M5d2/files/crates/loopd/tests/scheduler_heartbeat.rs @@ -65,11 +65,8 @@ fn a_due_heartbeat_runs_on_the_background_slot_with_a_time_line() { let sent = server.requests_to(CHAT); assert_eq!(sent.len(), 1); assert_eq!(sent[0].json()["id_slot"], 1); - let last = sent[0].json()["messages"] - .as_array() - .unwrap() - .last() - .unwrap(); + let body = sent[0].json(); + let last = body["messages"].as_array().unwrap().last().unwrap(); assert_eq!(last["role"], "user"); let content = last["content"].as_str().unwrap(); assert!( diff --git a/docs/plans/M5d2/files/crates/loopd/tests/session_slot.rs b/docs/plans/M5d2/files/crates/loopd/tests/session_slot.rs index b4db135..d70b542 100644 --- a/docs/plans/M5d2/files/crates/loopd/tests/session_slot.rs +++ b/docs/plans/M5d2/files/crates/loopd/tests/session_slot.rs @@ -161,7 +161,7 @@ fn a_heartbeat_turn_between_two_main_turns_does_not_change_the_main_prefix() { assert_eq!(sent[1].json()["id_slot"], 1); assert_eq!(sent[2].json()["id_slot"], 0); let first = sent[0].json()["messages"].as_array().unwrap().clone(); - let third = sent[2].json()["messages"].as_array().unwrap(); + let third = sent[2].json()["messages"].as_array().unwrap().clone(); assert!( third.len() > first.len(), "the main session's second turn is a longer prefix"