Skip to content

feat(now): the body double proposes one tiny, passive first move (#30) - #32

Merged
NetDevAutomate merged 39 commits into
mainfrom
feat/body-double-first-move
Sep 22, 2026
Merged

NetDevAutomate merged 39 commits into
mainfrom
feat/body-double-first-move

Conversation

@NetDevAutomate

@NetDevAutomate NetDevAutomate commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

Issue #30 — the owner's note beside rubric row 3b (a): the sit-with session must not be a blank page. When the now engine proposes sitting with the plan (item 5's body-doubling floor, v0.5.0), the proposal carries one tiny, passive first move on the deferred material — and, after rubric row 3c was scored reading by reading with the owner (2026-09-21), the move became a property of the recommendation, not only of the sit-with.

The two shapes it takes, from one definition (_first_move_sentence):

Low energy, sit with the plan — Open your Frames material and read for ten minutes, nothing more — no indexed lesson mentions “window frame” yet.
Medium/high, a plan-related repair — Open “Advanced Sql 4H” from Complete Sql Databases Bootcamp — the match is the phrase “window function” — and read for ten minutes, then start the repair.

Base build

  • f43f7eb2 RED → 4d8d9fd5 GREEN — decision._first_move + the lesson-lookup seam; metadata["first_move"]; CLI First move: line; Today card line + firstMove in body-double-request; Body Double picker #bd-first-move; both guides; openspec change body-double-first-move.

Rubric row 3c, one reading per turn (receipt: docs/architecture/plan-integration/receipts/now-rubric-2026-09-16.md)

  • (a) the kind of move — yes. Reading, passive, capped.
  • (b) the wording — yes, with a requirement: a deliberate lesson should always be the case. RED 2e1c7ea7. Building it against the owner's real vault showed the concept→title→topic fallback chain always names a lesson — the wrong one (Frames → a PySpark lab) — which reopened (c).
  • (c) when no lesson matches — concepts only; otherwise name the milestone and say why. RED fdfe1672 → GREEN 62f3d6f8: the seam searches the deferred milestone's own concepts and nothing else, raises on an unreadable index; three honest no-lesson shapes.
  • (d1) the sentence appeared twice — keep the First move line, drop it from the reason. RED 630cac72 → GREEN 75bc9b7a: the reason ends at the co-study guarantee; metadata.first_move is the only carriage.
  • (d2) "Open X" actually opens X — build the button, gated behind the evidence sentence. The owner's refined self-check ("I would likely still open it in case there was some link being enforced") set the gate: a named lesson is followed, not merely doubted. Cycle 1 RED 2b85a80b+089d09ae → GREEN 4459ea38: the seam returns (lesson_id, title, course, concept) and the sentence states its evidence. Cycle 2 RED e1576276 → GREEN 266afd4f: Open the lesson on the Today card and the Body Double picker opens the Course Explorer aside beside the view via explorer-open-lesson / openLessonById.
  • (d3) the move survives the start — carry it into the live session strip. RED 74779bda → GREEN 5666f141: #bd-live-first-move beneath the activity name for the whole session, the control beside it; confirmEnd() clears the move with the activity it arrived beside. Rejected: auto-open at start.
  • (e) the medium-energy control — owner: no, offer the move at medium too, built as (e1) the warm-up into the primary, on its own material (owner took the steer over a passive alternative beside the primary). RED fa5d76be → GREEN dacbea87: _warm_up on the primary only — plan-related, not the body double, action_type in hands-on/conversation/teachback — three tails (repair → then start the repair; eligible milestone → its own concepts, then start the milestone; other → then start on “”); never recall (the resolver is not asked), never off a plan, never an alternate; at any energy. The CLI needed no change (it keys on metadata.first_move); the Today card did not — see (e2).
  • (e2) the warm-up follows Start into the Study view — owner: build it, the (d2)+(d3) shape. Two defects found by reading the code the warm-up had to travel through: the Today card's Start → on a study action handed the Study picker nothing (not the move, not even the concept — only the resume and parked paths carried a topic), and firstMoveNote()/firstMoveLesson() gated on source === 'body_double', so the card did not render the warm-up at all — dacbea87's "renderers unchanged" was true of the CLI and false of the card; corrected in design decision 11 and the receipt. RED eb3be21a → GREEN 25886ff0: the gate comes off; Start hands {topic, energy, firstMove?, lesson?} over the existing today-resume; the Study picker shows the move beneath the topic (#study-first-move, #study-first-move-open) and the live layout beneath the status bar for the whole session (#study-live-first-move, #study-live-first-move-open); cleared with the topic on end and on a planning launch; behaviour-tested on the session-timer.js/today-panel.js node harnesses.

Deliberately not built: a first move for a deferred repair (unreachable — design decision 2); speaking the move (the co-study persona is untouched; #33 holds the read-along idea); a reader pane inside the live session (#33's).

Cost: lesson lookup 861 ms cold (the explorer's index refresh) / 45–58 ms warm per concept; warm-up path re-measured 161 ms cold / 45 ms warm. Paid once per now whose primary carries a move; a now with no active plan pays nothing.

Recorded, not fixed here: the medium screen's "last seen 8 day(s) ago" for a row planted 3 days before the frozen clock — now_world freezes decision.datetime only while history/progress.py reads the wall clock. A test-isolation gap, not a product defect; its own small change.

Council review 8 (2026-09-21) — findings landed one commit each

  • The move never outlives the material it arrived beside — the finding all three seats converged on (astra F1/F2 🔴, grok 🟡, qwen 🟡). Editing or re-picking the material in either picker, a second hand-off during a live or starting session, and a reattached session all kept or swapped the move beneath different material. One clearFirstMove() writer per view, guards at every material-changing transition, behaviour-tested on the Study harness, pinned statically on Body Double (c5065cf0, design decision 12). Kept against grok's counter (a typo-fix in the plan title loses a correct move): the move is one Start away.
  • A too-short concept is unsearchable, not a searched miss (astra F3 🟡) and the first WELL-FORMED hit wins (a malformed top row no longer hides the lesson beneath it — astra refutation 3, grok 🔵): a33249da → d029b0d6.
  • The record no longer claims "body double only" (astra F4): 357ee258.
  • A blank-concept plan-related primary carries no warm-up (grok 🔵; reachable through a topic match — the RED printed Open your “” material …): 79761a94 → 193b541d.
  • A learning row's warm-up ends then start the review, not the repair — my own finding while verifying qwen's recall claim; the same contradiction row 3b (c) removed from the reason: 024373ba → f8783a73.
  • Refuted with evidence (in the arbitration): deferred title vs resolved concepts are one milestone by construction; the planning launch already cleared the move; no retrieval test is emitted as an active type; the plans-view hand-off already clears. Rejected: the 0.5.1 version pin (the release change's); .picker-hint reuse (CI green on the suites that query it). Owner's call, unchanged: the high-energy ramp (decision 10). Separate change: the frozen-clock gap.
  • Process: the seat transcripts were recovered a day late from a gitignored run directory after the coordinating session was interrupted — recorded in the arbitration's process findings, with the originals' hashes in review8/manifest.json. A RED test that armed the real tick without destroy() made node --test hang to its timeout; fixed in c5065cf0.

Gates before merge

  • Owner scores rubric row 3c — all five readings scored 2026-09-21 (receipt b64afd02).
  • Council review 8 (three seats) — ran 2026-09-21 on c83ebd75 once the gateway's provider credential was rotated (astra ACCEPT-WITH-CORRECTIONS, grok ACCEPT-WITH-CORRECTIONS, qwen ACCEPT); GATE ACCEPT for f8783a73 in docs/architecture/plan-integration/council/review-8-arbitration-2026-09-21.md, seat transcripts in council/review8/. No finding changed a scored screen.
  • CHANGELOG [Unreleased] entry (865e686a).
  • openspec archive body-double-first-move before the 0.5.1 cut.
  • CI green; fast-forward main.

Tested

At f8783a73/865e686a (after council review 8): JS 164/164 (15 s); first-move pins + plan-integration docs contract + now-guidance + learning-decision + web-now 148 passed; the web unit suites that read the markup 30 passed; ruff, ruff format, pyright 0; mkdocs build --strict clean; openspec validate valid (the lifetime scenario and two sentences added); golden ec451ce8 byte-identical. Full pytest on the tree: 7314 passed, 31 failed, 14 errors — the 44 failing/erroring ids under packages/studyloop are exactly the committed sandbox-environmental set (receipts/full-suite-control-item4-2026-09-18.md; set difference ∅ both ways, item 4's seven then-REDs green), plus one order-dependent id outside this branch's files (packages/agent-session-tools/tests/test_sync_conversation_integrity.py::test_concatenated_remote_dump_with_existing_archive, red in the whole-tree run, green alone, green with its file, green with its whole package 2115/2115; the branch touches nothing under packages/agent-session-tools).

At 25886ff0/c83ebd75: new Study pins + Body Double pins + docs contract + now-guidance + web-now + golden 134/134; the web unit suites that read the markup 144 passed (1 skipped, 5 deselected); JS 160/160; node --check on the edited scripts; golden now_plan_no_active.json byte-identical (ec451ce8) throughout; ruff, ruff format, pyright (0 errors); mkdocs build --strict clean; openspec validate body-double-first-move valid (two requirements, fifteen scenarios). Every RED committed failing for its stated reason before its GREEN; the RED expectations corrected at GREEN (the medium payload's key shape in dacbea87; the F7 "dispatches nothing" assertion and the async harness in 25886ff0) are recorded in those commits. Real-vault probes of the shipped seam: window frame → none; window function → Advanced Sql 4H / Complete Sql Databases Bootcamp; decorators → Decorators 29M / The Ultimate Typescript.

#30)

The owner's note beside rubric row 3b (a): the sit-with session must not be
a blank page. Today _body_double_candidate names what was deferred and
proposes nothing; the co-study persona stays quiet by default, so the
learner sits down to a plan title.

Eleven tests, all red for the stated reason and none for a fixture error:

- engine: metadata["first_move"] is the sentence "Open the <milestone>
  material and read for ten minutes, nothing more.", closes the reason as
  "A first move, if you want one: …", contains no exercise, and adds no
  top-level JSON key (golden untouched); when the content index resolves the
  milestone's concepts to a lesson, the lesson is named and the index is read
  once; a raising lookup degrades to the milestone title with no warning.
- CLI: a "First move:" line beneath "Sit with the plan:".
- Today card: firstMoveNote(rec) and the body-double-request detail carrying
  firstMove beside activity and energy (additive; the existing hand-off pin
  is unchanged).
- Body Double view: static pins on components.js (listener reads
  detail.firstMove, firstMove: '' in state) and index.html (#bd-first-move
  under the activity; .today-first-move line on the card) — the monolith has
  no node harness, so the contract is pinned in place.
- docs: both guides describe the move as passive and optional.

The lookup seam is named decision._lesson_title_for so tests can plant a
lesson without an FTS index. A body double always has a deferred milestone to
draw on — an eligible milestone would have been synthesised as a plan-related
candidate and suppressed it — so the issue's "deferred repair only" row is
unreachable and is deliberately not built.
GREEN for f43f7eb. When the engine proposes sitting with the plan, the
proposal now carries one first move on the deferred material — "Open the
Frames material and read for ten minutes, nothing more." — derived from the
first named plan's deferred next milestone and, when the explorer's FTS
resolves the milestone's concepts to an indexed lesson, that lesson's title.
It rides as metadata["first_move"] on the body-double recommendation only
(the Recommendation dataclass is unchanged, so the no-plan golden ec451ce8 is
byte-identical) and closes the reason as "A first move, if you want one: …".

Why passive: the move must be consumable at the capability the day carries
(design §5, low = 3/10), so it is reading — never an exercise, never a
Socratic round — and the co-study persona's silence rule is untouched: the
Body Double view shows the text, nobody says it.

Why the milestone and only the milestone: a body double is synthesised only
when no plan-related candidate exists, and an eligible next milestone would
have been synthesised as one, so every plan behind a body double has its
next milestone deferred. The issue's "deferred repair only" row cannot occur
and is recorded as not built rather than shipped as dead code.

The lesson lookup (_lesson_title_for) is a seam and a refinement: lazy import
of the explorer FTS so learning does not import web at load, one short query
per concept, any failure answers None and the move names the milestone. Cost
on the live host: 861 ms cold (the explorer's index refresh), 45–58 ms warm
per concept, paid only when a body double is synthesised.

Renderers read the field and re-derive nothing: CLI prints "First move:"
beneath "Sit with the plan:"; the Today card renders its own line and hands
firstMove to the Body Double view in the body-double-request detail beside
activity and energy (additive — a payload without it hands over exactly what
it did before); the picker shows it beneath the activity. Both guides state
it in the engine's terms, pinned by the docs contract.

Verification: the eleven RED tests green; 444 passed across the now/decision/
MCP/docs/web subsets; JS 150/150; ruff, pyright and mkdocs --strict clean.
The openspec change body-double-first-move carries the spec delta (one ADDED
requirement, four scenarios), design and tasks; it is archived once the
owner scores rubric row 3c.
…d9fd, verdict PENDING (#30)

Row 3's world through both real collectors on the GREEN tree: the sit-with
proposal carries "A first move, if you want one: Open the Frames material and
read for ten minutes, nothing more." The lesson lookup answers nothing here,
as it does on the owner's own vault for "window frame" (checked). Medium
energy is the control: guided repair primary, no body double, no first move.
The owner scores the row; council review 8 is recorded as blocked at the
model provider on all three seats after the key rotation and is re-run once
the gateway authenticates.
Copilot AI lite review requested due to automatic review settings September 21, 2026 09:50
@NetDevAutomate NetDevAutomate added this to the 0.5.x milestone Sep 21, 2026

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

Reset stale firstMove state after sessions and add focused lesson-resolution path coverage.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds an optional passive first move to plan-aware Body Double proposals across CLI and web interfaces.

Changes:

  • Derives lesson- or milestone-based guidance.
  • Carries it through Today and Body Double UI.
  • Adds tests, documentation, OpenSpec artifacts, and rubric evidence.
File Summary
packages/​studyloop/​tests/​test_web_body_double_first_move.py Pins static Body Double UI contracts.
packages/​studyloop/​tests/​test_now_plan_guidance.py Tests engine and CLI guidance.
packages/​studyloop/​tests/​test_docs_plan_integration_contract.py Validates documentation contracts.
packages/​studyloop/​tests/​js/​today-panel-plan.test.js Tests UI handoff behavior.
packages/​studyloop/​src/​studyloop/​web/​static/​js/​components/​today-panel.js Passes first-move guidance to Body Double.
packages/​studyloop/​src/​studyloop/​web/​static/​index.html Adds first-move markup.
packages/​studyloop/​src/​studyloop/​web/​static/​components.js Stores and displays handoff state; reset stale firstMove when the session ends.
packages/​studyloop/​src/​studyloop/​learning/​decision.py Derives first-move guidance; focused helper-path coverage is requested.
packages/​studyloop/​src/​studyloop/​cli/​_now.py Renders the CLI guidance.
openspec/​changes/​body-double-first-move/​tasks.md Tracks implementation and release tasks.
openspec/​changes/​body-double-first-move/​specs/​active-learning-decisions/​spec.md Defines the requirement and scenarios.
openspec/​changes/​body-double-first-move/​proposal.md Describes scope and exclusions.
openspec/​changes/​body-double-first-move/​design.md Records implementation decisions and cost.
openspec/​changes/​body-double-first-move/​.openspec.yaml Declares change metadata.
docs/​web-ui-guide.md Documents the Body Double experience.
docs/​study-plans.md Documents plan-aware guidance.
docs/​architecture/​plan-integration/​receipts/​now-rubric-2026-09-16.md Records rubric row 3c evidence.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

/* Issue #30: the proposal's one passive first move, shown beneath the
activity so the session does not open on a blank page. Cleared when
a hand-off carries none, so a stale move never outlives its plan. */
this.firstMove = detail.firstMove ? String(detail.firstMove) : '';
…long

Owner, 2026-09-21, adopting the steer as his reasoning: his own note made
literal; reading sits at zero in the demand classes so 3/10 refuses nothing;
'nothing more' caps the commitment. Two follow-ons: open the material inside
StudyLoop's own frame (to reading (d)); read-along mode filed as #33.
… the index holds one (rubric 3c (b))

Owner, 2026-09-21, scoring reading (b): 'A deliberate lesson should always be
the case — this stops any decision fatigue and removes that friction.' The
seam becomes _resolve_lesson(queries) -> (lesson_id, title) | None, asked once
with the fallback chain in order — the milestone's concepts, then its title,
then the plan's topics — so a vault that never uses the concept's exact words
still yields a specific lesson; the id rides as metadata['first_move_lesson_id']
so a renderer can open it in StudyLoop's own frame. The milestone form remains
only for an index that holds nothing relevant. A seam test proves the chain
against the explorer's search function faked at its boundary. The two
pyright-ignore tags mark attributes the GREEN defines; they come off then.
…e's own concepts, otherwise says why (rubric 3c (c))

Rubric row 3c reading (c), owner 2026-09-21. Building (b)'s "always a
deliberate lesson" chain (concepts -> milestone title -> plan topics) and
running it against the owner's real vault showed it always names a lesson —
the wrong one: "Frames" FTS-hits a PySpark data-frames lab, "sql" hits an
SQL bootcamp introduction; only the sibling milestone's "window function"
finds Advanced Sql 4H, and only because this plan's two milestones share a
lesson. A deliberate-but-wrong lesson spends a 3/10 day's one action on the
wrong material and looks certain doing it. Owner: "I suspect I would have
doubts/concerns before opening which were confirmed after opening it."

So the search is bounded to the deferred milestone's own concepts, and when
they match nothing the sentence names the milestone and says why — the
information the fix needs (a lesson, or a concept name on the milestone):

  Open your Frames material and read for ten minutes, nothing more — no
  indexed lesson mentions “window frame” yet.

Seven tests fail against the working tree's chain for the stated reasons:
the real-vault replay stops at "Frames" and names the PySpark lab; a
concept-less milestone must not be looked up by its title; a searched miss
names the missing concept(s); an index that cannot be read makes no claim
about itself (the seam raises instead of answering a false miss); the CLI
prints the why-clause. The seam test's ignore tags come off at GREEN.
…cepts only, otherwise names the milestone and says why (rubric 3c (b)+(c))

GREEN for fdfe167 (rubric row 3c readings (b) and (c), owner 2026-09-21).

_resolve_lesson(concepts) asks the explorer's FTS one short query per
concept of the deferred milestone, first hit wins, returning (lesson_id,
title) so a renderer can open the lesson in StudyLoop's own frame
(metadata.first_move_lesson_id). It no longer swallows failures: None is a
searched miss, an unreadable index raises out of the seam, and the caller
tells the two apart.

_first_move names the lesson when a concept resolves — (b): a deliberate
lesson whenever the vault holds one — and otherwise names the milestone AND
says why, in one of three honest shapes (design decision 6):

  searched, no match  … nothing more — no indexed lesson mentions “window frame” yet.
  no concept named    … nothing more — this milestone names no concept to look up yet.
  index unreadable    … nothing more.   (no claim about an index never read)

The (b) chain that also searched the milestone's title and the plan's
topics is dropped, not shipped: on the owner's real vault it named a
PySpark data-frames lab for "Frames" and an SQL bootcamp intro for "sql" —
always a lesson, the wrong one. Recorded in the design with the
measurement; tasks T3b/T3c carry the revision.

Verification: test_now_plan_guidance.py 82/82 (the seven RED tests green,
no other test changed behaviour); JS 150/150 with the fixture sentence
updated to the emitted one; docs contract tests, mkdocs --strict, openspec
validate clean; ruff, pyright clean; no-plan golden ec451ce8 byte-identical.
Emitted for the receipt through both real collectors and the real seam: the
seam was asked ("window frame",) only; on the owner's vault "window frame"
resolves to nothing today, "window function" to Advanced Sql 4H.
…ts only, otherwise name the milestone and say why; screen re-emitted from 62f3d6f

Owner verdicts recorded verbatim, with the real-vault measurement that turned (b)'s chain into (c) and the self-check answer. World, emitted and what-it-does columns amended to the shipped seam (asked the milestone's own concept only) and the three honest no-lesson shapes.
…so closing the reason (rubric 3c (d))

Owner verdict 2026-09-21 on rubric row 3c reading (d1): keep the dedicated
'First move' line, drop the sentence from the reason. As emitted on 62f3d6f
the same twenty-odd words appeared twice on a 3/10 screen — closing the Why:
paragraph and again as their own line beneath the door — because the first
RED specified both. The reason explains the recommendation; the move is an
action and belongs beside the door. No consumer reads the reason alone: the
CLI, the Today card and MCP get_next_action all carry metadata.first_move.

- the engine test requires the reason to end at the co-study guarantee and
  to contain neither the sentence nor the words 'first move'
- the CLI test requires the sentence exactly once on the panel
- the real-vault test's wrong-lesson check now reads the whole serialised
  recommendation, since the reason no longer carries a lesson name at all
…m the reason (rubric 3c (d1))

Owner verdict 2026-09-21, rubric row 3c reading (d1): keep the dedicated
'First move' line, drop the sentence from the reason.

The first GREEN (4d8d9fd) closed the body double's reason with 'A first
move, if you want one: <move>' AND carried the move as metadata.first_move,
so the CLI panel and the Today card each showed the same twenty-odd words
twice on a 3/10 screen — the Why: paragraph's tail and the dedicated line two
lines below. The reason explains the recommendation; the move is an action
and belongs beside the door. Verified before removing the copy: no consumer
reads the reason alone — CLI now, the Today card and MCP get_next_action all
carry the payload's metadata — so nothing loses the move.

- decision.py: the reason tail is gone; metadata['first_move'] is the only
  carriage (comment records the rule and why)
- spec delta: the reason SHALL NOT carry the sentence; each renderer shows
  it once; scenario 1 pins the reason's ending; scenario 3's wrong-lesson
  check reads the whole serialised recommendation
- design decision 5 records the verdict and the verification
- tasks: T3d
- docs/study-plans.md: 'beside the session door, once'

Re-emitted through both real collectors with the real seam: the low panel
carries the sentence exactly once, the medium control zero times; golden
ec451ce8 byte-identical; engine + decision 87/87, docs contract and static
pins green, JS 150/150, openspec valid, mkdocs --strict clean.
…e reason; screen re-emitted from 75bc9b7

Owner verdict 2026-09-21. The emitted reason now ends at the co-study
guarantee; the sentence is counted once on the low panel and zero times on
the medium control. (d2)-(d3) and (e) remain to walk.
…on is opened despite the doubt

Verbatim: 'Before opening it — but honestly, I would likely still open it in
case there was some link that is being enforced between PySpark and SQL'. A
named lesson carries implied authority, so a wrong lesson is followed, not
merely doubted. Recorded as the constraint on (d2): the sentence must state
the evidence for the lesson it names before any control opens it.
…longs to and the concept the match rests on (rubric 3c (d2))

Owner, refining the (c) self-check while (d2) was open (2026-09-21):
'Before opening it — but honestly, I would likely still open it in case
there was some link that is being enforced between PySpark and SQL'. A
named lesson carries implied authority: a wrong lesson is followed, not
merely doubted. So the sentence must let the link be judged from the
sentence — and the button (d2) sits behind that.

- the lesson sentence becomes: Open “<lesson>” from <Course> — the match
  is the word/phrase “<concept>” — and read for ten minutes, nothing more.
  The course is the hit's own course_id humanised exactly as the explorer's
  course list shows it (stored fact, same function); the concept is the one
  the FTS matched.
- the seam returns (lesson_id, title, course) and skips a hit lacking its
  course or title: a lesson is named with its evidence or not at all.
- metadata.first_move_lesson_title rides beside first_move_lesson_id so a
  renderer can open the lesson with its name, without parsing the sentence.
- replays the owner's real-vault finding through the real seam: a Python
  plan's 'decorators' resolving to Decorators 29M in The Ultimate TypeScript
  now reads as a word match at a glance.
…ched

With two concepts the caller cannot know which one hit; the sentence needs
it ('the match is the word X'). The hit is (lesson_id, title, course,
concept) — the concept that matched, not the first one asked.
… to and the concept the match rests on (rubric 3c (d2))

Owner, 2026-09-21, refining the (c) self-check: 'Before opening it — but
honestly, I would likely still open it in case there was some link that is
being enforced between PySpark and SQL'. A named lesson carries implied
authority, so a wrong lesson is followed, not merely doubted. The sentence
must let the link be judged from the sentence.

- _resolve_lesson returns (lesson_id, title, course, concept): the course is
  the hit's own course_id humanised by the explorer's _humanise, exactly as
  its course list shows it; the concept is the one that matched, not the
  first asked. A hit lacking its course or title is skipped — a lesson is
  named with its evidence or not at all.
- _first_move: Open “<lesson>” from <Course> — the match is the word|phrase
  “<concept>” — and read for ten minutes, nothing more. Returns the lesson
  title too; metadata.first_move_lesson_title rides beside the id so a
  renderer opens the lesson by name without parsing the sentence.
- spec delta: requirement text, scenario 2 with evidence, a new scenario
  for the TypeScript case, the seam scenario's four-part hit and the
  skipped orphan row; design decision 7 records the owner's words, the
  mechanism, and why topic scoping was not chosen; docs/study-plans.md.

Engine + decision 88/88, docs contract and static pins green, JS 150/150,
openspec valid, mkdocs --strict clean, golden ec451ce8 byte-identical.
… in the Course Explorer beside the view (rubric 3c (d2))

Owner verdict 2026-09-21: build the open-the-lesson control, gated behind the
evidence sentence (landed in 4459ea3). Eight tests, red for their stated
reasons:

- Today card (node): firstMoveLesson(rec) exposes {id, title} only for a
  body-double proposal that resolved a lesson; openFirstMoveLesson()
  dispatches explorer-open-lesson {lessonId, title} and does NOT navigate —
  the learner stays on Today, the lesson opens in the aside; startPrimary
  hands firstMoveLessonId/firstMoveLessonTitle to the Body Double view
  beside the move (a payload without them hands over exactly what it did).
- static pins (components.js has no node harness): the explorer listens for
  explorer-open-lesson and openLessonById() opens the aside and the lesson;
  the Body Double listener stores the id and title, the view has
  openFirstMoveLesson(); the picker has #bd-first-move-open and the Today
  card data-testid=today-open-first-move-lesson, each shown only when a
  lesson resolved and placed beside the move.
- docs: the Today and Body Double guide sections say the lesson opens in
  the Course Explorer.
…he Course Explorer beside the view (rubric 3c (d2))

Owner verdict 2026-09-21: build the button, gated behind the evidence
sentence (4459ea3). Before this, first_move_lesson_id was carried and
consumed by nothing: the card said 'Open “Decorators 29M”' beside a UI that
could have opened it and left the finding to the learner.

- Course Explorer: listens for explorer-open-lesson {lessonId, title};
  openLessonById opens the aside if closed (same store/layout toggle as
  toggle()), fetches the tree if needed, and calls the existing openLesson
  with the minimal lesson object openSearchResult builds — no second reader.
- Today card: firstMoveLesson(rec) exposes {id, title} only for a body-double
  proposal that resolved a lesson; openFirstMoveLesson() dispatches the event
  and navigates nowhere — the learner stays on Today with the lesson in the
  aside; startPrimary hands firstMoveLessonId/Title beside firstMove.
- Body Double view: stores the id and title from the hand-off (cleared with
  the move); openFirstMoveLesson(); #bd-first-move-open beside the move.
- index.html: both controls use the stylesheet's bulk-btn; each x-shows only
  when a lesson resolved — row 3's Frames world shows no button.
- docs/web-ui-guide.md (Today, Body Double); spec scenario; design decision
  8 (why the aside, why the in-session pane is #33's); tasks T3e.

Static pins + docs contract + engine 123 green, JS 153/153, node --check on
both scripts, mkdocs --strict clean, openspec valid, golden ec451ce8
byte-identical. Wide web subset 782 passed with two failures outside this
change: the live_kiro dogfood test (opt-in live agent) and a schema-init
test that passes alone and 7/7 in its own file.
…ce sentence; real-vault seam probe recorded

Owner verdict 2026-09-21. Cycle 1 (2b85a80+089d09ae -> 4459ea3): a named
lesson states its course and the matched concept. Cycle 2 (e157627 ->
266afd4): Open the lesson on the Today card and the Body Double picker,
opening the Course Explorer aside beside the view. Shipped seam probed on
the real index: 'window frame' -> None (no button, honestly); 'decorators'
-> Decorators 29M / The Ultimate Typescript. (d3) and (e) remain.
…the first move and its control beneath the activity name (rubric 3c (d3))

Owner 2026-09-21, rubric row 3c reading (d3): carry the move and the button
into the live session strip.

Why: the first move and Open-the-lesson live only in the picker
(x-show="!sessionActive && !starting"). Pressing Start hides the picker and
shows a strip with the activity name and End — the sentence is gone at
exactly the moment the blank page arrives, and unless the lesson was opened
beforehand there is no second chance without ending the session.

Four pins, each failing for its stated reason before a line of code changed:
- #bd-live-first-move in the strip, x-show/x-text="firstMove", after
  #bd-live-activity (beneath the activity name);
- #bd-live-first-move-open beside it, x-show="firstMoveLessonId", reusing
  openFirstMoveLesson() — the view's one opener;
- confirmEnd() clears firstMove/firstMoveLessonId/firstMoveLessonTitle beside
  the activity it already clears: the move arrived with the activity in one
  hand-off and leaves with it, so the strip never shows a stale move beneath
  the next, unrelated activity;
- the Body Double guide says, at the Start step, where the move lives once the
  session runs and that the companion never says it.

The state itself already survives the start — nothing clears the three fields
on startSession() — so GREEN is markup reading state the view holds, one
end-path line, one CSS rule and one guide sentence. No auto-open at start.
…irst move and its control beneath the activity name (rubric 3c (d3))

Owner 2026-09-21, rubric row 3c reading (d3): carry the move and the button
into the live session strip. GREEN for RED 74779bd.

What changed:
- index.html: #bd-live-first-move inside .bd-live-strip, x-show="firstMove",
  the same sentence, with #bd-live-first-move-open beside it when a lesson
  resolved (x-show="firstMoveLessonId", @click="openFirstMoveLesson()" — the
  view's one opener). The strip is flex-wrap, so the line wraps to its own
  row beneath the activity name and End.
- style.css: one rule, flex-basis 100%, typed like the picker's hint so the
  sentence reads as the same proposal it was before Start.
- components.js: confirmEnd() clears firstMove/firstMoveLessonId/
  firstMoveLessonTitle beside the activity it already cleared. The move
  arrived with the activity in one hand-off and leaves with it — now that the
  strip shows the move for the whole session, a stale one beneath the next,
  unrelated activity would be a confidently wrong proposal on the one surface
  that is always on screen.
- docs/web-ui-guide.md: the Start step says where the move lives once the
  session runs and that the companion never says it.
- spec delta: scenario "The move survives the start"; design decision 9
  (rejected: auto-open at start); tasks T3f.

Nothing the companion says changes — the co-study persona is untouched; the
move stays a proposal on screen. No payload change, so the golden is
byte-identical.

Verification: static pins + docs contract 39/39 (the four RED flipped), engine
+ decision + web-now + golden 89/89, JS 153/153, node --check on
components.js, mkdocs --strict clean, openspec valid, golden ec451ce8
unchanged.
…ve session strip; RED 74779bd → GREEN 5666f14

Owner verdict 2026-09-21 recorded beside (d2) with the evidence put to the owner (both lived only in the picker; Start hid them at the moment the blank page arrived), the steer, the rejected auto-open, the one consequence built (confirmEnd() clears the three first-move fields beside the activity), and the unchanged row-3 screen. (e) remains.
… too; medium control re-emitted from 5666f14; (e1) shape pending

Verdict recorded verbatim before any build. Also records the wall-clock leak on the medium screen ("last seen 8 day(s) ago" for a row planted 3 days before the frozen clock): now_world freezes decision.datetime only, history/progress.py reads the real clock. A test-isolation gap to fix separately, not a product defect.
…t move on its own material (rubric 3c (e1))

Owner 2026-09-21, rubric row 3c reading (e): "no, offer the move at medium
energy too", built as (e1) — the warm-up INTO the primary, on the primary's
own material (the owner took that steer over the passive alternative beside
the primary).

Why: the low-energy move is the sit-with's whole action and ends "nothing
more"; printed beneath a task the day CAN carry, that sentence would tell the
learner two contradictory things and the passive one is the easier to take
(the 3b (d) mistake). A ramp ends "then start …" and lowers the first step of
the primary instead of competing with it.

Five tests fail for their stated reason before a line of code changed:
- the row-3 repair at medium (both collectors live) carries
  metadata.first_move = Open "Advanced Sql 4H" from Complete Sql Databases
  Bootcamp — the match is the phrase "window function" — and read for ten
  minutes, then start the repair.; lesson id + title beside it; the resolver
  asked ("window function",) only; reason untouched; no top-level key added;
- a plan milestone primary asks the milestone's own concepts, all of them, and
  ends "then start the milestone";
- nothing indexed: Open your "window function" material … then start the
  repair — no indexed lesson mentions "window function" yet.; an unreadable
  index gets the plain ramp and no warning;
- the CLI prints First move: beneath Record evidence: at medium, once;
- both guides say a plan-related active action carries a warm-up ("then
  start", "warm"), and that recall never carries one.

The sixth new test is the GUARD, holding today and required to hold at GREEN:
no warm-up on a plan-related recall (the resolver is not asked — reading the
lesson before a retrieval test defeats the test), none on an active primary
off every plan, and the no-plan world byte-identical to the golden.
… on its own material (rubric 3c (e1))

Owner 2026-09-21, rubric row 3c reading (e): "no, offer the move at medium
energy too" — built as (e1) the warm-up INTO the primary, on its own material
(the owner took that steer over the passive alternative beside the primary).
GREEN for RED fa5d76b.

Why this shape: the low day's move is the sit-with's whole action and ends
"nothing more". Printed beneath a task the day CAN carry, that sentence would
tell the learner two contradictory things and the passive one is the easier
to take — the 3b (d) mistake. The warm-up ends "then start …" and lowers the
first step of the primary instead of competing with it. On the medium screen
the window function repair resolves on the owner's vault to Advanced Sql 4H in
Complete Sql Databases Bootcamp — the lesson where window functions are
taught — so the ramp reads: Open "Advanced Sql 4H" from Complete Sql Databases
Bootcamp — the match is the phrase "window function" — and read for ten
minutes, then start the repair.

What changed (decision.py):
- _first_move_sentence: ONE definition builds both moves — (b)'s deliberate
  lesson, (c)'s three honest no-lesson shapes, (d2)'s evidence sentence —
  parametrised by material and tail. _first_move (the sit-with) now calls it
  with tail "nothing more"; its output is unchanged (every earlier first-move
  test green).
- _warm_up: on the primary only, after ranking and the plan-backed guarantee;
  plan-related (carries a PlanRef), not the body double, action_type in
  hands-on/conversation/teachback. Tails in test order: a repair
  (energy_demand in metadata — both collectors mark repairs and nothing else)
  → "then start the repair"; the plan's eligible next milestone → its own
  concepts, all of them, "then start the milestone"; any other plan-related
  active item → "then start on “<concept>”".
- Never recall (the resolver is not asked — reading the lesson before a
  retrieval test defeats the test; row 3b (b) already said familiar recall
  leads as it is); never visual/audio (already passive); never off a plan, so
  the no-plan golden ec451ce8 is byte-identical; alternates carry none.
- _first_move_metadata: the shared carriage (first_move, lesson id, title),
  used by the body double too — same keys, same order.
- Renderers needed no change: CLI First move:, the Today card's line and its
  Open-the-lesson control key on metadata.first_move, not on the body double.

Also: both guides (study-plans "Plan-aware now", web-ui-guide "Today"); a new
spec requirement with three scenarios (moved after the first requirement's
scenarios — the validator caught the orphaning); proposal corrected (the
reason has not carried the move since (d1); the warm-up is a second carrier);
design decision 10; read cost re-measured for the warm-up path (161 ms cold /
45 ms warm); tasks T3g.

RED correction made at GREEN, recorded: the medium payload has no
energy_deferred* keys (omitted when empty), so its top-level shape is the
golden's plus active_plans — the RED's assertion listed all three. One test
added at GREEN for the third tail (a plan-related teach-back), labelled so.

Verification: now-guidance + learning-decision + web-now + golden + static pins
+ docs contract 135/135, JS 153/153, ruff + format + pyright clean, mkdocs
--strict clean, openspec valid, golden ec451ce8 unchanged.
…material; RED fa5d76b → GREEN dacbea8; row 3c fully scored

Owner took the steer over the passive alternative. Records the sentence as the engine test replays it with the vaults real hit, the isolated-world emission with its fixture-index caveat (as at (d2)), the scope rule with each exclusions reason, and the DoD. Council review 8 remains blocked at the model provider.
…r and status bar carry it, the Today card renders it (rubric 3c (e2))

Owner 2026-09-21: "build it now, the (d2)+(d3) shape on the Study view".

Two defects behind this RED, both found by reading the code the (e1) warm-up
had to travel through:
- The Today card's Start → on a study action navigated and handed the Study
  picker NOTHING — not the warm-up, not even the concept. Only the resume and
  parked paths (today-resume) carried a topic. The learner retyped the topic
  from memory and the sentence the card had just shown was thrown away.
- firstMoveNote()/firstMoveLesson() on the Today card gate on source ===
  'body_double', so the card did NOT render the (e1) warm-up at all. The (e1)
  GREEN (dacbea8) claimed "renderers needed no change" — true of the CLI,
  which keys on the field, false of the Today card: I read the field names,
  not the two method bodies. Corrected here; recorded in the receipt and the
  design at GREEN.

Twelve tests fail for their stated reasons (the flashcards guard holds):
- today-panel-plan.test.js: the two source-gate assertions flipped (the card
  reads the field wherever the engine put it); Start on a study-session primary
  dispatches today-resume {topic, energy, firstMove, firstMoveLessonId,
  firstMoveLessonTitle}; without a move, {topic, energy} only; a flashcards
  primary still hands nothing and navigates.
- session-timer.test.js: today-resume carries the move and lesson beside the
  topic and a hand-off without one clears them; openFirstMoveLesson dispatches
  explorer-open-lesson (nothing to open → nothing dispatched); confirmEndSession
  clears the move with the topic; startPlanning carries no warm-up.
- test_web_study_first_move.py (new): #study-first-move beneath #topic-input
  with #study-first-move-open; #study-live-first-move beneath the status bar
  with #study-live-first-move-open.
- docs contract: the Study Session guide names the first move, the status bar
  and the control, and where the move comes from; Today says Start hands the
  action to Study Session.
… status bar carry it, the Today card renders it (rubric 3c (e2))

Owner 2026-09-21: "build it now, the (d2)+(d3) shape on the Study view".
GREEN for RED eb3be21.

Two defects fixed:
- today-panel.js startAction(): Start on a study action navigated and handed
  the Study picker NOTHING — not the (e1) warm-up, not even the concept. It
  now dispatches today-resume {topic: concept, energy, firstMove?,
  firstMoveLessonId?, firstMoveLessonTitle?} — the existing resume/parked
  event, no new one — then navigates. The Body Double hand-off is unchanged
  and shares one _firstMoveDetail builder.
- today-panel.js firstMoveNote()/firstMoveLesson(): the source ===
  'body_double' gate comes off; the card reads the field wherever the engine
  put it. This gate hid the (e1) warm-up, so dacbea8's claim that the Today
  card "followed without change" was false — true of the CLI only. Recorded in
  design decision 11 and the receipt; decision 10 corrected in place.

session-timer.js: firstMove/firstMoveLessonId/firstMoveLessonTitle state; the
today-resume listener sets them from EACH hand-off (a resume or parked pick-up
carries none and clears one left by an earlier Start); openFirstMoveLesson()
dispatches explorer-open-lesson (nothing to open → nothing dispatched);
confirmEndSession() clears them with the topic; startPlanning() clears them
(the architect interview is not a repair).

index.html: #study-first-move + #study-first-move-open beneath the topic input
in the picker; #study-live-first-move + #study-live-first-move-open beneath
the status bar in the live layout, for the whole session. style.css: one
block. docs/web-ui-guide.md: Study Session steps 2 and the status-bar
paragraph; Today's Start sentence. Spec: requirement text corrected, scenario
"The warm-up follows Start into the Study view". Tasks T3h. Proposal.

Test-side changes recorded: two JS assertions flipped (the card renders the
move for any recommendation that carries one); the review-7 F7 test's "an
ordinary action dispatches nothing" updated to the new hand-off; the (e2)
harness keeps its stubs until an async run settles (first version restored
them in a synchronous finally).

Verification: new pins + Body Double pins + docs contract + now-guidance +
web-now + golden 134/134; web unit suites reading the markup 144 passed (1
skipped, 5 deselected); JS 160/160; node --check on both scripts; mkdocs
--strict clean; openspec valid; golden ec451ce8 unchanged.
…view; (e1) renderer claim corrected in place; RED eb3be21 → GREEN 25886ff

Records the two defects (Start handed the Study picker nothing; the Today card gated the move on the body double), the owner instruction, the build, and the correction of the (e1) record: the Today card did not follow without change — the claim was asserted from field names, not method bodies.
…earched miss; a malformed top row does not hide the hit beneath it

Two accepted findings from council review 8 (seats openai.gpt-6-astra,
grok-4.6, qwen3-coder; brief docs/architecture/plan-integration/council/
brief-review8-2026-09-21.md), verified against the tree before acceptance:

- astra F3 (🟡): _resolve_lesson skips queries under two characters (the
  explorer's search refuses them) but the caller then said "no indexed lesson
  mentions “C” yet" — a searched-miss claim about a search that never ran.
  None must stay a SEARCHED miss: a too-short concept is named as too short,
  and a miss names only the concepts that were searched.
- grok 🔵 / astra refutation 3: the seam fetched ONE row per concept and, when
  that row lacked its course or title, skipped the whole concept — so a
  malformed top row hid a well-formed lesson ranked beneath it. "A hit lacking
  its course or its title SHALL be skipped" means skip the ROW.

Both fail for their stated reason before a line of code changed.
… a searched miss; the first WELL-FORMED hit wins

GREEN for RED a33249d. Two findings, verified against the tree before
acceptance:

- astra F3 (🟡): the explorer refuses queries under two characters and the
  seam skipped them, yet the sentence said "no indexed lesson mentions “C”
  yet" about a search that never ran — a false content-gap claim in the one
  clause whose whole point is honesty. _first_move_sentence now separates
  searchable from too-short concepts (one shared constant, _MIN_QUERY_CHARS):
  too-short ones are named as "too short for the index to look up" and never
  sent to the resolver; a miss names only the concepts that were searched.
- grok 🔵 / astra refutation 3: the seam fetched ONE row per concept and, when
  it lacked its course or title, gave up on the concept — a malformed top row
  hid a well-formed lesson beneath it. It now fetches _FTS_ROWS_PER_CONCEPT
  (3) and names the first well-formed row; a concept whose only rows are
  malformed still answers None.

Spec delta amended: first WELL-FORMED hit wins; rows are skipped, not
concepts; the too-short clause added; the resolver scenario states the
skipped-row case. RED fixture corrected at GREEN and recorded: the plan store
refuses a second write of one id, so the mixed-concepts case became its own
test with its own plan id (assertions unchanged); both tests re-run red
against the pre-fix source before this commit.

Verification: now-guidance + learning-decision + web-now + golden + docs
contract green, ruff/format/pyright clean, openspec valid, golden ec451ce8
unchanged.
…arrived beside

GREEN for RED a33249d's sibling on disk: the one finding all three seats
converged on (astra F1/F2 🔴, grok 🟡, qwen 🟡). Decisions 9 and 11 cleared
the move on two transitions — end of session, hand-off without a move — and
claimed "a stale move never outlives its plan". Three transitions were
unguarded, each putting a material-specific proposal beneath DIFFERENT
material:

- the learner edits the material in the picker (#topic-input,
  #bd-activity-input) or chooses another target from a picker select or the
  target-kind select — the Frames sentence and its Open-the-lesson control
  stayed beneath whatever was typed and rode into the session;
- a second hand-off arrives while a session is live or starting — both
  listeners wrote the three fields unconditionally, and the same fields feed
  the LIVE strip, so a request for B installed B's move (or nothing) under A's
  running session;
- reattachConflictSession() adopts a session that is not the hand-off's, and a
  pending picker move sat beneath it for the whole session.

Each view now has one writer for the empty state, clearFirstMove(), and every
material-changing transition calls it: onTopicEdited()/onActivityEdited() on
the inputs (the Study input keeps its job of un-selecting the suggestion),
selectOption() for every picker select, @change on #target-kind-select, the
end paths (routed through the helper; the existing static pin accepts either
form), both reattaches. Both listeners return before the move fields when
sessionActive || starting, still pre-filling the picker's topic/activity and
energy as before. Kept against grok's counter (a typo-fix in the plan title
loses a correct move): cheaper than a confidently wrong proposal on the
surface that is always on screen.

Harness fact: reattachConflictSession() arms the real one-second tick and the
RED test did not destroy() it, so `node --test` never exited the file — the
run hung to the timeout rather than failing. Fixed in the test.

Spec: the lifetime rule and a scenario. Design decision 12. Guide sentence.

Verification: JS 164/164 (15 s, was a 120 s hang), pins + plan-integration
docs contract + now-guidance + learning-decision + web-now 146/146, web unit
suites reading the markup 30 passed, node --check both scripts, openspec
valid, mkdocs --strict clean, golden ec451ce8 unchanged.
…ove is body-double only

astra F4 (🔵): design decision 3 said the lookup cost is paid "only on the
body-double path", decision 4 that the metadata is "present only on the body
double", and the Today card comment that the line is "hidden for every other
action" — all three true when written at (a) and false since the (e1) warm-up.
Each is annotated as history amended by decisions 10–11 (kept as history, not
rewritten), and the markup comment says what the gate removal made true.

No code change; pins, docs contract, web unit suite and openspec re-run green.
…cept carries no warm-up

grok 🔵 (and astra, "requested checks"): _warm_up builds the repair and generic tails from (primary.concept,) whatever the concept is. A candidate is plan-related by concept, topic OR course (_candidate_keys), and the struggle collector does not guard row["concept"], so a blank-concept active item can be the plan-related primary — and the warm-up minted is "Open your “” material and read for ten minutes, then start on “” — “” is too short for the index to look up." (verified: that exact string is what this RED prints). Nothing to name, nothing to search: no warm-up, resolver not asked. Fails for its stated reason before a line of code changed.
…arries no warm-up

GREEN for RED 79761a9 (grok 🔵; astra's "requested checks" named the same
path as UNVERIFIED). _warm_up now orders its tails so that the two which name
and search primary.concept — the repair and the generic ramp — require one,
the milestone tail still uses the milestone's own concepts through its ref,
and a blank concept that reaches neither returns None: no first_move, no
lesson, resolver not asked. Precedence for a named concept is unchanged
(repair, then milestone, then generic), so every earlier warm-up test holds.

Reachable, not hypothetical: _candidate_keys matches a plan on concept, topic
OR course, and the struggle collector does not guard row["concept"], so the
RED printed the exact sentence the finding predicted — "Open your “” material
… — “” is too short for the index to look up."

Spec sentence added beside the third tail.

Verification: now-guidance + learning-decision + web-now 99/99 (golden
ec451ce8 unchanged), ruff/format clean, pyright 0, openspec valid.
…epair

Coordinator finding while verifying qwen's recall-exclusion claim (council review 8, recorded as my own, not a seat's): every struggle row carries energy_demand, so _warm_up gave a learning row's teach-back the repair tail — "then start the repair" beneath a reason that says "Recorded as learning; a gentle review keeps it fresh". Row 3b (c) removed exactly that contradiction from the reason. The warm-up must speak the row's own vocabulary: learning is a review, only struggling is a repair. Fails for its stated reason before a line of code changed (the RED prints the repair tail verbatim).
GREEN for RED 024373b (coordinator finding at council review 8, recorded as
my own). The demand-marked tail chooses its noun from the row: "then start
the review" when metadata.confidence is "learning", "then start the repair"
otherwise — the same vocabulary the row's reason already uses since row 3b
(c) ("Recorded as learning; a gentle review keeps it fresh"). Precedence and
every other tail unchanged.

Design decision 10's "both collectors mark repairs and nothing else" was
false — every struggle row carries energy_demand — and is corrected in place
(kept as history, annotated), with the docstring and the spec's first tail.

Verification: now-guidance + learning-decision + web-now 100/100 (golden
ec451ce8 unchanged), ruff/format clean, pyright 0, openspec valid.
…fest, arbitration; rubric row 3c and T4 record it

The council ran on 2026-09-21 (tree c83ebd7) once the owner rotated the
gateway's provider credential, but the coordinating session was interrupted
mid-GREEN and its continuations timed out, so nothing of the run reached the
repo. The seat outputs survived only in a gitignored run directory on the main
checkout; they are copied here (review8/seat-*.md; the pre-commit whitespace hooks normalised trailing whitespace and final newlines, git diff -w against the originals is empty, originals' sha256 in the manifest), with the
brief actually sent (93.9 KB — the 44 KB draft written before readings
(b)–(e2) existed is dropped) and a manifest built from the run's own
spend.json and run.log.

review-8-arbitration-2026-09-21.md: seats and verdicts (astra and grok
ACCEPT-WITH-CORRECTIONS, qwen ACCEPT); every finding's disposition with its
commit and test — accepted: the lifetime class (c5065cf), too-short concept
+ first well-formed hit (a33249d→d029b0d6), F4 record (357ee25), blank
concept (79761a9→193b541d), and my own learning-row tail (024373b→
f8783a7); refuted with evidence: deferred-vs-next milestone, planning
launch, recall exclusion, plans-view hand-off; rejected: 0.5.1 pin,
.picker-hint reuse; owner's: the high-energy ramp; separate: the clock gap.
GATE ACCEPT for f8783a7. Process findings: copy seat outputs into the repo
the turn they arrive; a hanging JS suite reads as a timeout.

Receipt row 3c: the "still blocked" clauses become the result, with the
effect on the scored screens stated (none — the pinned low and medium
sentences pass unchanged on f8783a7). tasks.md T4 ticked with the evidence.

Verification on f8783a7: JS 164/164; pins + docs contract + engine + web-now
148 passed; markup-reading web suites 30 passed; ruff/format clean; mkdocs
--strict clean; openspec valid; golden ec451ce8 unchanged.
…he whole chain

T6, first item, written in the same session as the work rather than deferred: one entry covering the sit-with move, the deliberate lesson with its evidence sentence, the honest no-lesson shapes, the warm-up into a plan-related primary, the renderers and the Open-the-lesson control, the Start hand-off the Study picker used to lack, and the lifetime rule from council review 8. tasks.md T6 records what of it is done (entry, PR, CI on the reviewed head) and what waits (CI on the council head, fast-forward, archive).
…ain spec

Every task is ticked and every artifact done, and the archive condition
the change set for itself — rubric row 3c scored by the owner — is met:
walked interactively on 2026-09-21, one reading per turn, (a) through
(e2), each verdict and self-check answer recorded in the receipt. T5 and
T6 are ticked with what actually happened (T6: the fast-forward of main
and its push are the owner's step, after which #30 closes with the merge
sha). Archived with the CLI's own merge: +2 requirements added, 0
modified, 0 removed, to active-learning-decisions — the body double's
passive first move and the plan-related primary's warm-up — verified
afterwards against the delta: both requirements present, the delta's
body a verbatim block of the main spec, scenarios 66 -> 81 (+15, the
delta's own count), no other spec touched. `openspec validate --specs
--all` 24/24; `--archived` validates the new archive (the one archived
failure, 2026-07-12-complete-e2e-harness-and-desktop-mcp with four
open tasks, predates this branch and fails identically on main).

This is the release gate's expectation: a change with commits since
v0.5.0 must be archived or deferred, and #30 ships in 0.5.1.
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