feat(now): item 5 (D-F) — energy demand for repair and the body-doubling floor - #28
Merged
Merged
Conversation
… install.sh The `installer` job in nightly-install.yml (A4, b5339db, 2026-09-14) has failed every night since it was added (runs 34949338879 .. 35431368317). It isolates HOME to `${{ runner.temp }}/home` so `studyloop install agents` writes into scratch, but an empty HOME has no harness: on the runner no codex/opencode/pi/grok binary is on PATH and no ~/.kiro, ~/.claude, ~/.pi or ~/.grok exists, so `detect_available_agent_tools()` returns [] and `install.sh` exits 1 at "Installing agent definitions" — the script's documented behaviour when no supported AI tool is present. The two tools the job was written to prove had already installed fine by then; the verify step never ran. Reproduced locally with an empty HOME and a PATH holding only uv and the system bins: exit 1, same message. Planting ~/.kiro (or ~/.claude, ~/.pi, ~/.grok — the markers the detector reads without a binary) makes the same command exit 0 and write the links. This test pins the fixture: the run step plants a marker before the script, and a verify step reads what `install agents` wrote into the isolated HOME. Red on the current workflow for that reason (1 failed, 13 passed).
…d HOME GREEN for ee43863. The `installer` job isolates HOME so `install agents` writes into scratch; the fixture now creates ~/.kiro, ~/.claude, ~/.pi and ~/.grok in that HOME first — the four markers detect_available_agent_tools() reads without a binary — so `install.sh` reaches and exercises all four link sets instead of refusing with "No supported AI tools detected". A new step verifies what `install agents` wrote: two artefacts each for kiro, claude and grok, one for pi, every path taken from installers.py (_TOOL_LINKS / _configure_*). Without a read of the isolated HOME the isolation proved nothing. Proved locally the way the runner runs it: full ./scripts/install.sh --non-interactive --no-smoke with HOME, UV_TOOL_DIR and UV_TOOL_BIN_DIR in scratch and a PATH holding only uv and the system bins — exit 0, "Installation complete!", both entry points answer, all seven artefacts present. Contract module 14/14. The nightly run itself can only be observed at 03:30 UTC or via workflow_dispatch on main after merge.
…tus posted, left open Both issues carry the proposals' content with every cited test id, symbol and path verified against main a03fc9b before posting. #21 stays open because its definition of done names OpenCode in the core tuple and the harness receipt records OpenCode as preview on two named blockers.
…doubling floor Rubric row 3 was the owner's one "no" on the D-16 walkthrough (2026-09-16): at low energy the engine recommended hands-on repair of a LIVE struggle, because a struggle-repair candidate carried no energy demand of its own — rule 3 only ever gated new milestone work. Recommending that on a low-energy day risks compounding the struggle (RSD). Design §5 plus its three T5.1 amendments (read against decision.py on 2026-09-18) is the spec these six tests pin: - demand is derived in the struggle collector from the collector's own classes: `struggling` seen within 14 days → high (asks for 6/10); older `struggling`, or a row whose only signal is a weak teach-back → medium (4/10); `learning` → low (0/10); carried in the candidate's metadata; - below the capability, repair above its demand is deferred like new work into a NEW additive key `energy_deferred_repairs` (`DeferredRepair`: plan_id/plan_title when plan-related, else None) — `energy_deferred` is milestone-shaped and its three renderers would print "milestone None"; - due recall is never deferred whatever its confidence says; - when nothing plan-related fits and an active plan exists, one `source="body_double"` conversation candidate is synthesised, base below MILESTONE_BASE_SCORE, plan_refs (plan, None), reason naming the deferred items, evidence_command the co-study session door (`studyloop study "<title>" --mode co-study`) — a proposal, never a filter: a real unrelated candidate still wins and it sits beneath; - no active plan → no body double (a draft is not active); the deferral is plan-independent; when every real candidate was deferred the starter stands in and its reason says so rather than "no evidence found yet"; - each renderer gains one line per deferred repair (CLI now, recap sentence, Today card `deferredRepairNotes()`), a body-double primary shows "Sit with the plan" + its door instead of "Record evidence", and the Today card routes it to the Body Double view (`viewForAction`). The struggle collector runs for real over patched `observations.rows`, since injecting candidates through `_due_progress_candidates` would bypass the derivation under test. 6 failed / 40 passed (golden byte-identity green); JS 5 failed / 139 passed — each for the missing name it is written against.
…ling floor GREEN for ef319a7. Rubric row 3 (owner: "no") said a struggle-repair task carried no energy demand of its own, so low energy recommended hands-on repair of a LIVE struggle. Now: Engine (learning/decision.py) - `_struggle_candidates` derives `energy_demand` once, from its own row classes, and carries it in metadata: `struggling` seen within LIVE_STRUGGLE_DAYS (14) → high (6/10); older `struggling` or a weak teach-back alone → medium (4/10); `learning` → low (0/10). An unreadable `last_seen` on a struggling row is read as live — the cautious side. - `_defer_repairs` (rule 3 extended) runs before rule 6 so a deferred repair no longer "represents" a milestone; repair above its demand is listed in the NEW additive key `energy_deferred_repairs` (`DeferredRepair`, plan_id/plan_title when plan-related else None) and never ranked. Due recall is never deferred whatever its confidence. Plan-independent. - `_body_double_candidate`: when nothing plan-related fits and a matchable active plan exists, one `source="body_double"` conversation candidate, base 30 (< MILESTONE_BASE_SCORE 48; +12 bias = 42 < any real candidate), plan_refs (plan, None) per plan, reason naming every deferred milestone and repair, evidence_command the co-study door `studyloop study "<title>" --mode co-study` set explicitly (T5.1 amendment 3: `_evidence_command` would answer with a progress write). - Starter after a deferral says the energy deferred the repair work, not "no evidence found yet" (false). Golden world defers nothing: unchanged. Renderers — one line per deferred repair, beside the milestone line - cli/_now.py: "Deferred for energy: … repairing “x” (confidence) asks for N/10; low energy carries 3/10"; a body-double primary's command is labelled "Sit with the plan", not "Record evidence". - learning/recap.py: a sentence per deferred repair in `_plan_context`. - today-panel.js: `deferredRepairNotes()`, counted in `hasPlanContext`; `viewForAction(rec)` starts a body-double primary in the Body Double view; index.html renders the lines and shows the block for them alone. Decisions taken at GREEN (design §5, recorded): deferral is plan-independent; rule 8's guaranteed slot below the floor is the body-double proposal (test_preserves_one_plan_backed_action_when_energy_allows updated — it advertises no work the energy cannot carry, primary untouched); honest starter; body-double shape; a deferred repair does not represent a milestone. Spec/docs: MODIFIED "The now engine is plan-aware with tested ranking rules" in the active-learning-decisions delta (rule 2's repair half, the body-double clause after rule 5, rule 7's slot, the new key, every current scenario carried by name plus five new ones); docs/study-plans.md "Plan-aware now" and docs/cli-reference.md say the same. Rubric row 3b written (three readings printed from the real engine; verdict PENDING for the owner). tasks.md T5.2/T5.3 ticked. Verification: test_now_plan_guidance 46/46 (golden byte-identical), test_learning_decision 5/5, JS 144/144 (+5), e2e plan journeys 20/20, docs contract 39/39, mkdocs --strict 0, openspec valid, ruff/pyright clean. Full suite vs clean main control: item5 − control = ∅, control − item5 = ∅ (44 shared sandbox-environmental ids, identical to the committed set); receipt docs/architecture/plan-integration/receipts/full-suite-control-item5-2026-09-19.md.
An active-but-unready plan is matched but never synthesised (spec rule 8), and the body-double proposal is a synthesis: with only a husk active and nothing plan-related fitting the day, nothing is proposed to sit with — the warning beside it already says "pause or repair". With a ready plan beside the husk the proposal names the ready one alone. Design §5 decision 6.
…s deferred Design §5 decision 1 makes repair deferral plan-independent, so D-5's "a learner with no active plan receives the pre-#10 payload byte for byte" now holds for a no-plan learner with NOTHING deferred; one with a live struggle deferred at low energy receives `energy_deferred_repairs` (and the starter, if nothing else was collected) where they used to receive the hands-on repair. The MODIFIED requirement, docs/cli-reference.md and docs/study-plans.md now say exactly that instead of "unchanged"; design §5 records the consequence and puts the scope question to council review 7.
Binding: D-F verbatim, rubric row 3 verbatim, design §5 with its T5.1 amendments and the six GREEN decisions, hard rules verified on the tree. Then the four commits, the agent's own T5.x report, every diff in the range in full, rubric row 3b as written, the control receipt, reference facts, and ten numbered check questions (scope of the plan-independent deferral is asked outright as (a)).
…review 7, F1) Astra's red, reproduced with a real /bin/sh: the body-double door built `studyloop study "<title>" --mode co-study` by replacing `"` with `\"` — presentation, not quoting — so a plan title such as `SQL $(touch pwned) Windows` executed its substitution when the offered command was pasted. `_evidence_command` had the same shape for every concept and topic the engine offers. One helper, `_shell_word`: plain text keeps the double-quoted form the golden pins byte for byte; text carrying `"`, `\`, `$`, a backtick or `!` is `shlex.quote`d. Both builders use it. Pinned by test_body_double_command_preserves_title_as_one_literal_shell_argument, which runs each command through /bin/sh against a stub `studyloop` that records argv: seven titles round-trip literally and no side effect fires. Stash-proved: 3 of 7 fail without the fix. Golden and every exact-command pin unchanged.
…ew 7, F5) Astra's must-fix: in rubric row 3b's own fixture the engine's DeferredMilestone.reason and the CLI's "Deferred for energy" line ended "plan-related review and repair stay available" one line above the line saying the repair is deferred. Both now promise only what rule 3 still guarantees — due recall and gentle review. Pinned as whole sentences by test_cli_milestone_deferral_does_not_promise_live_repair.
…Body Double view (review 7, F7)
Astra 🔵 / grok 🔵 / qwen 🟡: navigating to the Body Double view lost the
proposal's context — the learner had to pick what to sit with again while
the CLI door carried the plan title. `startAction` now dispatches
`body-double-request` {activity: <plan title>, energy} before navigating —
the event-not-storage shape `today-resume` already uses — and
`bodyDoubleSession.init` adopts it as the view's activity and energy band.
Nothing starts on its own; the learner still presses start. Pinned by two JS
tests (dispatch + navigation order; title resolution with fallbacks).
…hout one (review 7, grok) A no-plan learner whose live struggle was deferred at low energy saw a block headed "Your plans" holding only the deferred line. The label is now planNotesLabel(): "Your plans" once any note involves a plan, else "Set aside today". Pinned in today-panel-plan.test.js.
…a null last_seen (review 7, F4)
Five pins astra (F4) and grok (🔵) named: the 14-day live window is
inclusive (14 → high, 15 → medium) and a null or unparseable `last_seen` on
a struggling row reads as live; `learning` is low demand even with a weak
teach-back while an old struggle with one stays medium; the capability
matrix (low rejects medium+high, carries low; medium/high carry all); a
deferred sole representative lets rule 6 synthesise the eligible milestone's
conversation, never a hands-on or a body double (design §5 decision 5); two
ready plans yield one proposal with refs in plan order, both milestones and
repairs named, the door on the first title.
The boundary pin surfaced a pre-existing crash: `_struggle_candidates` sorts
rows by `row["last_seen"]`, and a legacy study_progress row with a NULL
last_seen raised TypeError, losing every struggle candidate. It now sorts
as the oldest (`row.get("last_seen") or ""`) and the demand derivation reads
it as live, the cautious side.
…eview 7, F2) Astra and qwen flagged 🔴, grok 💡: "base below MILESTONE_BASE_SCORE so every real candidate outranks it" was false after the day's adjustments — at low energy the proposal (30 + 12 = 42) outranks a hands-on task that energy penalises (48 − 14 = 34), while every due and conversation candidate still outranks it (reproduced through the engine at recall and conversation modality). Arbitrated as the energy rule doing what the owner's finding asked, not a filter: the CLAIM was the defect. Corrected in the constant's comment, the body-double docstring, the spec delta's rule-5 clause and design §5 decision 4; pinned by test_body_double_ordering_after_adjustments_follows_the_energy_rule; the judgement goes to the owner as rubric row 3b reading (e).
…w 7, F3/F6) Astra 🟡 F3: "no plan and nothing deferred → byte-identical" was still false, because every struggle-collector candidate now carries metadata.energy_demand at every energy, and older struggles and weak teach-backs defer too, not only live struggles. docs/cli-reference.md and docs/study-plans.md now say exactly what happens without a plan (the spec delta's paragraph landed with F2's edit of the same file); F6: the rule-7 husk reference is described.
…3b readings (d)/(e), T5.4 ticked Three seats over brief-review7 on tree 6d5a2d0: astra and qwen ACCEPT-WITH-CORRECTIONS, grok ACCEPT. Every red and yellow reproduced before acceptance and landed one commit each (d193525 .. bdf4d6c), or rejected here with the reason and the owner question that replaces it. The seats split 2-1 on the plan-independent deferral and 2-1 on the body double's ordering; both go to the owner as row 3b readings (d) and (e), printed from the real engine on bdf4d6c.
item5 5146 passed vs control 5120, 44 failing ids identical on both sides and to the committed environmental set — zero regressions after the seven review-7 corrections.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved critical and moderate findings remain in command safety, date consistency, payload contracts, and deferred-repair presentation.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
Adds energy-aware struggle repair deferral and body-doubling fallback across the decision engine, CLI, recap, web UI, documentation, tests, and CI.
Changes:
- Adds per-repair energy demand, deferral metadata, and body-double recommendations.
- Updates CLI, recap, Today UI, and command generation.
- Expands specifications, documentation, receipts, tests, and workflow checks.
| File | Reviewed changes and findings |
|---|---|
packages/studyloop/tests/test_now_plan_guidance.py |
Adds engine and renderer scenarios for energy deferral and body doubling. |
packages/studyloop/tests/test_learning_decision.py |
Updates starter test coverage. |
packages/studyloop/tests/test_ci_workflow_contract.py |
Adds nightly installer contract coverage. |
packages/studyloop/tests/js/today-panel-plan.test.js |
Tests Today-panel behavior and handoff dispatch. |
packages/studyloop/src/studyloop/web/static/js/components/today-panel.js |
Adds deferred-repair presentation and body-double navigation. |
packages/studyloop/src/studyloop/web/static/index.html |
Renders deferred-repair notes and starter-card content. |
packages/studyloop/src/studyloop/web/static/components.js |
Handles body-double context handoff. Nit (1 vote): add consumer integration/browser coverage verifying bodyDoubleSession adopts the activity and energy. |
packages/studyloop/src/studyloop/learning/recap.py |
Renders deferred repairs. Moderate (2 votes): normal recap uses medium energy, so relevant deferrals are not populated; thread selected energy or narrow the contract and add a production-path test. Moderate (1 vote): no-plan deferrals are labeled Plan:; use a separate label/field or adjust the wrapper. |
packages/studyloop/src/studyloop/learning/decision.py |
Implements energy demand, deferral, body doubling, and quoting. Critical (3 votes): the after_deferral starter command interpolates configured topics unsafely; build it through _evidence_command() and cover the starter path. Moderate (1 vote): starter rendering hides the deferred-repair reason. Moderate (1 vote): a second clock read can violate the same-instant date contract. Nit (1 vote): the new no-plan payload field contradicts the byte-for-byte output contract. |
packages/studyloop/src/studyloop/cli/_now.py |
Displays repair deferrals and body-double commands. |
openspec/changes/plan-integration-followons/tasks.md |
Records follow-on task progress and verification. |
openspec/changes/plan-integration-followons/specs/active-learning-decisions/spec.md |
Updates ranking and behavior requirements. |
openspec/changes/plan-integration-followons/design.md |
Documents implementation decisions. |
docs/study-plans.md |
Documents energy-aware plan guidance. |
docs/cli-reference.md |
Documents CLI behavior. |
docs/architecture/plan-integration/receipts/now-rubric-2026-09-16.md |
Records rubric readings. Nit (1 vote): qualify the 42 comparison and state the low-energy hands-on exception. |
docs/architecture/plan-integration/receipts/full-suite-control-item5-2026-09-19.md |
Records the control-suite comparison. |
docs/architecture/plan-integration/council/review7/seat-qwen3-coder.md |
Records review findings and dispositions. |
docs/architecture/plan-integration/council/review7/seat-openai.gpt-6-astra.md |
Records review findings and dispositions. |
docs/architecture/plan-integration/council/review7/seat-grok-4.6.md |
Records review findings and dispositions. |
docs/architecture/plan-integration/council/review7/manifest.json |
Records review metadata. |
docs/architecture/plan-integration/council/review-7-arbitration-2026-09-19.md |
Records arbitration and dispositions. |
.github/workflows/nightly-install.yml |
Isolates the installation harness and verifies agent installations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+695
to
+699
| reason = ( | ||
| "Today's energy deferred the repair work it cannot carry; " | ||
| "start with one small retrieval signal instead" | ||
| if after_deferral | ||
| else "No learning evidence found yet; start by creating one small retrieval signal" |
Comment on lines
+68
to
+72
| for repair in getattr(plan, "energy_deferred_repairs", ()): | ||
| where = f" of {repair.plan_title}" if repair.plan_title else "" | ||
| sentences.append( | ||
| f"Repairing {repair.concept}{where} waits for more energy: it asks for " | ||
| f"{repair.required_capability} of 10 and today's energy carries " |
… and describes its door truthfully Applied from the rubric-3b council (2026-09-20), verified against source: - the framework rule "never name a struggle without an adjacent strength" (agents/shared/audhd-framework.md, Naming Struggle Topics) -- the reason named the deferred struggle with nothing beside it, though PlanSummary already carries milestone_done/milestone_total; a second pin says no progress sentence is ever invented when nothing is done (already true); - "no new material, no repair" promised a door the engine does not control; the co-study persona guarantees "the student drives" and "stay quiet by default", so the reason should say that, and a third pin holds the persona to it. One red for the intended reason; the other two are guards that stay green.
…scribes its door truthfully GREEN for a094637. From the rubric-3b council (2026-09-20), applied only where the finding held against source: - lead with a strength that exists: "N of M milestones of <plan> done." from PlanSummary.milestone_done -- the framework rule for naming deferred work -- and nothing when nothing is done (never invented); - describe the door by what its persona guarantees ("you drive; the companion stays quiet unless you ask") instead of a promise nothing pinned. Docs, spec delta and design (decision 9) say the same; the decision also records what was NOT applied (plan-gated deferral, unnamed deferrals) and reading (f), the same-concept due-recall collision two seats named.
…d; reading (f) added Three seats (gpt-6-astra, grok-4.6, qwen3-coder) were asked for a recommendation per reading with its basis, opposing case and falsifier -- not for verdicts; the rubric question is the owner's. The brief arbitrates them and lists what was applied (c519bc2), what was not and why, and which seat claims were checked and found wrong. The rubric receipt: the rationale's stale "42 < any real candidate" (review 7 F2 never reached it) corrected; (a)/(e) re-emitted from c519bc2; reading (f) -- the live struggle that is also due for recall, named by two seats independently -- emitted from the tree and added with its owner question.
…learning row is a gentle review Two defects the owner's rubric row 3b reading (c) surfaced while checking the door behind the yes (interactive walkthrough, 2026-09-20), one test each: 1. `_evidence_command` names `--type structured` for every teach-back candidate. Demand `low` (0/10, design §5 amendment 1) was justified by the protocol's *micro* teach-back — "in one sentence, explain [concept]", two dimensions scored — not by the 15-minute five-dimension structured review. The door must be the form the demand class stands on: `micro` for `low`; a weak-teach-back row (`medium`) keeps `structured`. 2. The struggle collector's reason for a `learning` row reads "Recorded as learning; repair now while the signal is fresh". Design §5 calls a `learning` row the gentle review that stays eligible at low energy, and on a low-energy screen the word "repair" contradicts the deferred-repair line printed beside it. The reason names the gentle review and its one-sentence door; a live struggle's reason is untouched. Both fail against c519bc2 for exactly those reasons.
…ng row reads as a gentle review GREEN for 0760254 (rubric row 3b reading (c), owner walkthrough 2026-09-20). - `_evidence_command` takes the candidate's `energy_demand` and names the teach-back form the demand class stands on: `--type micro` for `low` (the one-sentence, two-dimension teach-back that justified 0/10), `structured` otherwise — a weak-teach-back row (`medium`) re-sits the review it fell short on. Every other action type is untouched. - The struggle collector derives the demand once, before the candidate is built, and hands the same value to the door and to `metadata.energy_demand`. - A `learning` row's reason is "Recorded as learning; a gentle review keeps it fresh — one sentence, in your own words" (design §5: the gentle review that stays eligible at low energy); a `struggling` row keeps "repair now while the signal is fresh". The teach-back-score suffix is unchanged. The no-plan golden is byte-identical (ec451ce8): the golden world has no struggle rows. test_now_plan_guidance + test_learning_decision 76/76, test_web_now + test_wind_down_decision 14/14, ruff/pyright clean.
…ided-explanation fallback Rubric row 3b reading (c), owner verdict "yes, with a fallback" (walkthrough 2026-09-20): the micro teach-back is offered at low energy, and if the student cannot produce the one sentence the mentor moves to a guided explanation — not the four-round Stuck ladder first. Checked against the tree: socratic-engine.md's Stuck ladder reaches a worked example only at round 4, co-study.md offers a brief explanation after two exchanges, and teach-back-protocol.md has no "cannot produce → guided explanation" step; none of the three is conditioned on low energy. The door's protocol must say so, or the yes ships without its condition. Fails against f2cb572: the Micro Teach-Back section has no fallback.
…ation fallback GREEN for c7cc1b0 (rubric row 3b reading (c), owner verdict "yes, with a fallback", 2026-09-20). teach-back-protocol.md's Micro Teach-Back section gains the fallback the door now relies on: when `studyloop now` offers a micro teach-back on a low-energy day and the student cannot produce the one sentence, the mentor moves straight to a guided explanation (two or three plain sentences with a networking analogy, then one phrase said back in the student's own words) — not the four-round Stuck ladder, which at low energy is the productive struggle the day cannot carry and the RSD exposure the live-struggle deferral exists to prevent. The blank is not scored as a teach-back (it measures the day, not the concept); the phrase that came back is recorded as `learning` and the 7-day structured review measures the concept. agents/manifest.json: only the moved entry changes (hash 9bbe8831f1c74837 → 09ee534106322244, updated 2026-09-20); the updater's re-stamping of every unmoved entry's date was restored, as at item 4. .secrets.baseline: whole-repo scan with the hook's pinned v1.5.0 — 72 → 72 result files, exactly the manifest's one hashed entry moves.
…anded; (e)/(f) pending
Interactive walkthrough with the owner, 2026-09-20 — the row's verdict cell
as it stands, committed rather than left as a working-tree change:
(a) yes — sit with the plan rather than repair the live struggle at low
energy; note (0.6.0): the sit-with session needs one tiny concrete first
move, not a blank page.
(b) yes — familiar recall leads, the sit-with sits beneath it; note (0.6.0):
the low-energy day is a ladder, not a snapshot — offer a step-up after a
completed floor task (nothing re-plans after an action today).
(c) yes, with a fallback — the micro teach-back is offered at low energy and
the mentor falls back to a guided explanation when the sentence will not
come. Three fixes found while checking that door landed before this
commit: the door names --type micro for low demand (0760254/f2cb572f),
a learning row's reason is a gentle review not "repair now" (same pair),
and the protocol carries the low-energy fallback (c7cc1b0/214df377).
The cell's citation of design §5 is corrected to the design's own words
("recovered / gentle review → low"), not "consolidation".
(d) yes — the generic starter plus the deferred line is the no-plan floor;
note (0.6.0): re-offer the deferred repair itself as the step-up after a
completed floor task, keyed on an outcome signal that does not exist yet.
(e) and (f) remain PENDING for the owner; the row's top-of-file status line
is updated when they land.
…ith the repair
Found while emitting rubric row 3b reading (f) with both real collectors
(owner walkthrough 2026-09-20), executed not inferred: `_due_progress_candidates`
and `_struggle_candidates` read the same `observations.rows`; `_review_type_for`
labels every `struggling` row due ("Guided repair + tiny practice") and
`_action_for_review` makes that due item `hands-on` at 100 + days + 35. The
struggle collector's copy of the row is deferred at low energy (design §5);
the due collector's copy carries no `energy_demand`, is kept as "due recall",
and — deduped after the deferral — becomes the primary. Against a real
sessions.db, row 3's world at low energy therefore emitted:
window function — hands-on — "Guided repair + tiny practice; … struggling"
Deferred for energy: repairing "window function" (struggling) asks for 6/10
with no body double — the row-3 "no" still on top, its own deferral printed
beneath it. The (a), (d) and (e) behaviours the owner scored exist only with
the due collector silenced, which is what `_plant_struggles` does.
Two tests plant the rows for BOTH collectors: the plan world (body double
primary, the struggle named once in `energy_deferred_repairs`, medium energy
unchanged) and the no-plan world (starter primary). Both fail against
b161291 with `study_progress:…` as the primary's source.
…e repair GREEN for 8f851d3 (rubric row 3b reading (f), owner walkthrough 2026-09-20; design §5 decision 10). - `_due_progress_candidates`: a `struggling` row's due item ("Guided repair + tiny practice", `hands-on`) is the struggle collector's repair collected a second time from the same observations row. It now carries the same `energy_demand` (`_energy_demand` over `last_studied`), so rule 3 defers both copies together at low energy. Due recall and teach-back rows carry no demand and are never deferred — the invariant now stated about the rows it was always true of. - `_defer_repairs`: a concept is named once in `energy_deferred_repairs` however many copies of its row were deferred. - Spec: the "Due recall is never deferred" scenario is re-stated for a recall-typed due item, and a new scenario pins the struggling row read by both collectors (body double / starter primary at low energy, the due copy primary again at medium). Design §5: the rule-3 bullet amended, decision 10 records the mechanism from execution, decision 9's (f) note superseded. Re-emitted through both real collectors on this tree: (a), (b), (d), (e) unchanged from the receipt; (c) shows the due recall ("10-min Socratic review") first and the micro teach-back beneath it; (f) is not a recall/repair collision — a struggling row is never recall through the real collector — so it resolves to this fix. The receipt is amended in the following commit. No-plan golden byte-identical (ec451ce8). Suites 93/93, ruff/pyright clean.
…mitted through both collectors
Owner walkthrough 2026-09-20, closing row 3b:
(e) yes — sit with the plan rather than an unrelated hands-on drill at low
energy; the drill stays as the alternate.
(f) resolved by a fix, not a verdict — the posed recall/repair collision is a
world the real collectors never produce (a struggling row's due item is
the guided repair, collected twice). Emitting it exposed that the due
copy carried no demand, so against a real database the scored (a)/(d)/(e)
screens were never shown; the row-3 "no" was. Fixed 8f851d3/fb63fb6b.
The emitted column now says what each reading shows through BOTH collectors:
(a), (b), (d), (e) unchanged; (c) has the due recall first and the micro
teach-back beneath it (the fixture that silenced the due collector hid this
too); (f) before and after the fix. Status line and cell header updated.
…reen; T5.5 ticked Owner, 2026-09-20, on the (c) screen as shipped (due recall first, micro teach-back beneath): "With an energy score of 3/10, the initial task should be light to give confidence and hopefully by doing so energy, if the task is successful then offer the user the learning task which has a higher energy score." Recorded verbatim; it names the same ladder as the (b)/(d) notes (one 0.6.0 theme) and adds one observation for that item: the owner reads a teach-back as higher demand than a plain recall although both are `low` in the demand classes — the ordering already agrees, the classes govern deferral not order. Row 3b is fully scored: T5.5 ticked with the closure summary; the change is archived after PR #28 merges.
7 tasks
…t move) and #31 (the ladder)
NetDevAutomate
added a commit
that referenced
this pull request
Sep 20, 2026
CHANGELOG and releases/v0.5.0.md gain the four changes the owner's rubric row 3b walkthrough (2026-09-20) put on feat/energy-demand-body-double: - the item-5 entry no longer says "due recall is never deferred" as if it covered every due row — the due-review reader emits a struggling row as a hands-on guided repair, and that copy now carries the repair's demand and defers with it (the defect: a low-energy day showed the repair the learner was just spared as the primary, its own deferral printed beneath it); - the micro teach-back door for a `learning` concept and its gentle-review wording; - the teach-back protocol's low-energy guided-explanation fallback; - the verification note records that every row-3b reading was re-emitted through both readers before its verdict was read as a verdict on shipped behaviour. `just release-consistency-shipped` passes on this tree (pre-tag form).
…late one adopts a session into a settled picker The e2e test_409_from_a_second_tab click has timed out three times on CI (35341660469, 35380095596, 35516418191). The third run carried the capture added after the second, and its artifact named the mechanism: at the click, sessionActive was true, topic was tab A's "Study focus", conflictSession was null and the picker was display:none — with startSession() never run. The only click-less path that sets topic from server state is init()'s restore branch, and init() runs twice per page load (Alpine's auto-init plus the markup's x-init="init()"), each run issuing its own /api/session/state read. The settled wait observed the first; the second landed after the test's POST /api/session/start and adopted tab A's session into tab B. This test replays that world without a browser: two init() calls, the second state read held until a live session exists, then released. It fails today on both counts — two reads, and sessionActive flipped true on a picker the learner had settled on.
Alpine calls init() for an x-data object that defines one, and the markup also says x-init="init()", so the page called it twice. The earlier fix (#14 journey) made only the listener registration idempotent and left the fetches to run again as "idempotent". The RED before this commit shows they are not: two /api/session/state reads race, the picker settles on the first, and the second can land after a session was started elsewhere and adopt it — the CI artifact of run 35516418191, byte for byte. init() is now a once-per-instance gate over the former body (_initOnce): later calls return the first run's promise, so the settled picker is the one and only read, and the _listenersRegistered guard is subsumed. The e2e click-site comment records what the artifact showed and keeps the capture for any fourth occurrence, which would have to be a different mechanism. Verified on this branch: node --test 148/148 (147 + the RED, now green); the recovery journey e2e file 9 passed / 2 skipped in order; ruff clean.
This was referenced Sep 21, 2026
NetDevAutomate
added a commit
that referenced
this pull request
Sep 21, 2026
PR #28 (item 5 / D-F, the rubric-3b fixes, the session-timer init-once fix) landed on main by fast-forward on 2026-09-21. The release branch takes it as a merge commit rather than a rebase: the repository ruleset refuses force-pushes to release/* as well as main, so a rebased branch could not be published. Tree = main + the four release-preparation commits; the 0.5.0 notes already describe every change this merge brings in (b3eb9ac, 2b63bc7).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
Item 5 (D-F) of the plan-integration follow-on programme: per-item energy demand for struggle repair, and the body-doubling floor — the answer to rubric row 3, the owner's one "no" on the D-16 walkthrough (at low energy the engine recommended hands-on repair of a live struggle because a repair candidate carried no energy demand of its own).
learning/decision.py): the struggle collector derivesenergy_demandonce from its own row classes (struggling≤ 14 days → high 6/10; olderstrugglingor a weak teach-back → medium 4/10;learning→ low 0/10) and carries it inmetadata;_defer_repairs(rule 3 extended) defers repair above the day's capability into the new additive keyenergy_deferred_repairs(DeferredRepair, plan-related or not) and never ranks it; due recall is never deferred;_body_double_candidateproposes sitting with a ready plan when nothing plan-related fits (source="body_double", base 30, refs(plan, None), reason naming the deferred items, doorstudyloop study <title> --mode co-study); the starter says the truth after a deferral.now(one line per deferred repair; "Sit with the plan" for a body-double primary), recap sentence, Today card (deferredRepairNotes(),viewForAction→ Body Double view with the plan handed over bybody-double-request,planNotesLabel()).study-plans.md/cli-reference.md.$(…)executed when pasted; proved with a real/bin/sh) — and a pre-existing collector crash on a NULLlast_seenfound by a boundary pin. Arbitration:docs/architecture/plan-integration/council/review-7-arbitration-2026-09-19.md.receipts/now-rubric-2026-09-16.md). Scoring surfaced work, each landed RED → GREEN on this branch: (c)'s three conditions (the low-demand teach-back door is the micro form, alearningrow reads as a gentle review, the teach-back protocol carries a low-energy guided-explanation fallback); reading (f) was a world that cannot occur — the due-review reader collected the same live struggle a second time without its demand, so at low energy the just-deferred repair still led — fixed so that copy defers with the repair, named once; and the owner's two 0.6.0 notes are issues Plan integration follow-on: the body double proposes one tiny, concrete first move on the deferred material #30 and Plan integration follow-on: the low-energy day is a ladder — a step-up offered after a completed floor task, never led with #31.test_409_from_a_second_tab, run 35516418191, the first with a captured state): the timer'sinit()ran twice per page load, each run reading/api/session/state; the slower read could adopt a session started in another tab into a tab sitting on the picker. One run per page load now; pinned by anode --testreplay of the race.Commits (RED → GREEN → corrections, one per finding)
ef319a7bRED ·326abcf9GREEN ·8e9cbdf5ready plans only ·6d5a2d0ewording ·0fdda5c7brief ·d1935256F1 ·c1d7f2f2F5 ·3347567eF7 ·f814dc36label ·7194b66dF4 + nulllast_seen·02e282e6F2 ·bdf4d6c6F3/F6 ·3eb31f2darbitration ·849c78aareceiptRow 3b scoring (2026-09-20):
a0946376/c519bc2abody-double reason ·e770733ecouncil brief, (f) added ·0760254d/f2cb572fmicro door + gentle review ·c7cc1b0f/214df377low-energy fallback ·b1612918verdicts (a)–(d) ·8f851d3a/fb63fb6bdue copy of a live struggle defers with the repair ·291e8691(e) yes, (f) resolved, all six re-emitted through both collectors ·a4503a48(c) confirmed on the both-collectors screen ·90d749c6#30/#31 ·60d5f94c/96806febtimerinit()once per page loadVerification
test_now_plan_guidance.py71/71 (goldennow_plan_no_active.jsonbyte-identical), plan suites 96/96, JS 147/147, e2e plan journeys 20/20, docs contract 25/25,mkdocs --strict0,openspec validatevalid, ruff/pyright clean.maincontrol worktree on the final tree: 5146 passed vs 5120, 44 failing ids identical on both sides and to the committed sandbox-environmental set — zero regressions (receipts/full-suite-control-item5-2026-09-19.md).node --test148/148; the recovery-journey e2e file 9 passed / 2 skipped in order); the full-suite matched control is CI's on each push. CI was fully green (15/15) onb1612918,291e8691anda4503a48;90d749c6(docs-only) hit the e2e timeout this PR then fixed, and96806febcarries the fix.For the owner
Row 3b is scored (see Summary); nothing waits on the owner here. Merge is a local fast-forward of
mainafter CI is green on the head; #10 and #15 (and then #7) close on that merge; PR #29 (0.5.0) follows.