Skip to content

feat(now): item 5 (D-F) — energy demand for repair and the body-doubling floor - #28

Merged
NetDevAutomate merged 32 commits into
mainfrom
feat/energy-demand-body-double
Sep 21, 2026
Merged

NetDevAutomate merged 32 commits into
mainfrom
feat/energy-demand-body-double

Conversation

@NetDevAutomate

@NetDevAutomate NetDevAutomate commented Sep 19, 2026 •

Copy link
Copy Markdown
Owner

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).

  • Engine (learning/decision.py): the struggle collector derives energy_demand once from its own row classes (struggling ≤ 14 days → high 6/10; older struggling or a weak teach-back → medium 4/10; learning → low 0/10) and carries it in metadata; _defer_repairs (rule 3 extended) defers repair above the day's capability into the new additive key energy_deferred_repairs (DeferredRepair, plan-related or not) and never ranks it; due recall is never deferred; _body_double_candidate proposes sitting with a ready plan when nothing plan-related fits (source="body_double", base 30, refs (plan, None), reason naming the deferred items, door studyloop study <title> --mode co-study); the starter says the truth after a deferral.
  • Renderers: CLI 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 by body-double-request, planNotesLabel()).
  • Spec: MODIFIED "The now engine is plan-aware with tested ranking rules" (rule 2's repair half, the body-double clause, rule 7's slot, the honest byte-for-byte contract, five new scenarios). Docs study-plans.md / cli-reference.md.
  • Council review 7 (three seats over the full diff): astra and qwen ACCEPT-WITH-CORRECTIONS, grok ACCEPT → GATE ACCEPT after seven corrections, one commit each — notably F1: every offered command is now quoted as one literal shell argument (a plan title containing $(…) executed when pasted; proved with a real /bin/sh) — and a pre-existing collector crash on a NULL last_seen found by a boundary pin. Arbitration: docs/architecture/plan-integration/council/review-7-arbitration-2026-09-19.md.
  • Rubric row 3b — five readings printed from the real engine, walked through with the owner on 2026-09-20 and scored: (a)–(e) yes, (f) resolved by a fix (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, a learning row 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.
  • Session-timer init-once fix (found by this PR's third e2e timeout of test_409_from_a_second_tab, run 35516418191, the first with a captured state): the timer's init() 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 a node --test replay of the race.

Commits (RED → GREEN → corrections, one per finding)

ef319a7b RED · 326abcf9 GREEN · 8e9cbdf5 ready plans only · 6d5a2d0e wording · 0fdda5c7 brief · d1935256 F1 · c1d7f2f2 F5 · 3347567e F7 · f814dc36 label · 7194b66d F4 + null last_seen · 02e282e6 F2 · bdf4d6c6 F3/F6 · 3eb31f2d arbitration · 849c78aa receipt

Row 3b scoring (2026-09-20): a0946376/c519bc2a body-double reason · e770733e council brief, (f) added · 0760254d/f2cb572f micro door + gentle review · c7cc1b0f/214df377 low-energy fallback · b1612918 verdicts (a)–(d) · 8f851d3a/fb63fb6b due 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/96806feb timer init() once per page load

Verification

  • test_now_plan_guidance.py 71/71 (golden now_plan_no_active.json byte-identical), plan suites 96/96, JS 147/147, e2e plan journeys 20/20, docs contract 25/25, mkdocs --strict 0, openspec validate valid, ruff/pyright clean.
  • Full suite vs a clean main control 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).
  • 2026-09-20 heads: targeted suites green at each pair (engine 77/77 with the golden byte-identical; node --test 148/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) on b1612918, 291e8691 and a4503a48; 90d749c6 (docs-only) hit the e2e timeout this PR then fixed, and 96806feb carries the fix.
  • Not verifiable locally: CI's own matrix and e2e — this PR.

For the owner

Row 3b is scored (see Summary); nothing waits on the owner here. Merge is a local fast-forward of main after CI is green on the head; #10 and #15 (and then #7) close on that merge; PR #29 (0.5.0) follows.

… 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.
Copilot AI lite review requested due to automatic review settings September 19, 2026 12:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 High severity · 1 Medium severity

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 "
@NetDevAutomate NetDevAutomate added this to the 0.5.0 milestone Sep 20, 2026
… 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.
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.
NetDevAutomate added a commit that referenced this pull request Sep 20, 2026
The fix landed on PR #28 (96806fe) after the third CI timeout of the
two-tab 409 e2e test named its mechanism from the captured state. The
CHANGELOG says what the learner saw and what changed; the release note
keeps it to the one-line consequence beside the other correctness fixes.
@NetDevAutomate
NetDevAutomate merged commit 96806fe into main Sep 21, 2026
16 checks passed
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).
@NetDevAutomate
NetDevAutomate deleted the feat/energy-demand-body-double branch September 21, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants