S9: cluster lesson candidates on the topic line, not the headline #1289
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#1289
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
S9 lesson candidates cluster ruling moments on the stored headline, the first line of the AI message. On the real Store (355
memo_replymoments,nomic-embed-text, measured 2026-10-23) this gives 211 clusters: 205 singletons, one cluster of 60 moments over 43 sessions with agreement 0.70, and 0 candidates. The headline is usually a stock line, not the topic: 69 headlines start with## Predictionbecause the main session shows predictions under that heading, so they cluster together whatever their topic. Other stock first lines:## Short answer(8),You are right(9),**Short answer(4). Also,nomic-embed-textexpects a task prefix andPatternClusterersends none; the prefixclustering:alone gave 1 candidate (a 3-session cluster, agreement 1.0).Jared ruled option B-prime: the clusterer embeds
clustering:+ the topic text of each moment.Approach
Topic text of a moment, in order of preference:
Toward:value when the AI message has a PREDICTION block (format: StoreENTRY.mdsection 5, linesPREDICTION,Action:,Toward:, ...).#...) and not a stock opener (You are right,Short answerwith or without leading**).headline.Moment rows store only the headline (
DecisionMoment), so the topic text comes from the transcript at cluster time.MomentTopicreads each distincttranscript_pathonce throughExchangeFinder#pairsand finds the AI text by the moment'sai_index. No stored data changes;MomentLogandDecisionMomentdo not change.PredictionParser#toward(text)owns theToward:format: the value of the firstToward:line after aPREDICTIONline, or nil. The later YAML ticket for the predictor output then changes one method.Missing transcript: when
transcript_pathno longer exists (Errno::ENOENT, the same caseMomentExtractor#extract_and_advancerescues), the moment uses its stored headline. Reason: the moment is a real ruling with a stored outcome. Dropping it would silently shrink a cluster below 3 sessions, and raising would stop the refresh for every other moment because one transcript was cleaned up. The headline is a worse topic, but it is the only text the Store still holds.CandidateRefreshtakes acatalog:and buildsMomentTopicfromExchangeFinder.new(catalog);os-me-eval candidatespasses itscatalog.LessonCandidate#questionstill shows the cluster headline (out of scope).Required work:
PredictionParser#toward(text)that returns theToward:value of a PREDICTION block or nil — check: presentdef toward(text)inplugins/os-me/lib/os_me/prediction_parser.rbOsMe::MomentTopicthat returns one topic text per moment, falls back to the stored headline, and reads each transcript once — check: presentclass MomentTopicinplugins/os-me/lib/os_me/moment_topic.rbPatternClustererembedclustering:+ topic text instead of the headline — check: present'clustering: 'inplugins/os-me/lib/os_me/pattern_clusterer.rbPatternClusterer— check: absentrulings.map { |moment| moment['headline'] }inplugins/os-me/lib/os_me/pattern_clusterer.rbos-me-eval candidatesintoCandidateRefresh— check: presentcatalog: catalog).runinplugins/os-me/lib/os_me/cli.rbOrchestrator work: after merge, add one History row to
docs/implementation-status/os-me.md, then run a liveos-me-eval candidateson the real Store and report the cluster and candidate counts.Rejected: store a topic field on each moment row — changes stored data and
DecisionMomentfor a text the transcript already holds. Rejected: prefix only — measured 1 candidate, the## Predictioncluster stays.Acceptance criteria
Toward: cache the index in memory, whenCandidateRefresh#runruns with a fake embedder, then the embedder receivesclustering: cache the index in memory.## Prediction, then**Short answer:** yes, thenYou are right about the cache., thenDrop the second worker., when the refresh runs, then the embedder receivesclustering: Drop the second worker.clustering:followed by the moment's stored headline.transcript_pathdoes not exist, when the refresh runs, then it completes without error and the embedder receivesclustering:followed by the moment's stored headline.## Predictionand PREDICTION blocks whoseToward:texts the fake embedder maps to dissimilar vectors, when the refresh runs, then no candidate row is written.Toward:texts the fake embedder maps to the same vector, when the refresh runs, then one open candidate row holds the three moment ids.none) next to ruling moments, when the refresh runs, then the embedder receives no text for it.moments.jsonlis byte-for-byte unchanged.PREDICTIONline followed byAction: acceptandToward: use option B, whenPredictionParser#towardreads it, then it returnsuse option B; given a text with aToward:line but noPREDICTIONline, then it returns nil.All criteria run in Minitest under
plugins/os-me/test, with no model call and no network: transcripts and the Store are fixtures in a temp dir, and the embedder is a fake that records the texts it receives.Owner sketch
Retired tests
plugins/os-me/test/pattern_clusterer_test.rb
plugins/os-me/test/candidate_refresh_test.rb
Scope and blocking
In scope:
plugins/os-me/lib/os_me/(prediction_parser.rb, newmoment_topic.rb,pattern_clusterer.rb,candidate_refresh.rb,cli.rb) and tests underplugins/os-me/test.Out of scope:
MomentLog,DecisionMoment,MomentExtractor, stored Store data,OllamaEmbedder, the similarity threshold (0.85),LessonCandidateeligibility and question text, and the predictor output format change to YAML (separate ticket; it will change onlyPredictionParser#toward).Blocking: none. Blocks the S9 live refresh and the S9b One Thing question, which have no candidates to ask until this ships.
Origin
PatternClustererembedsmoment['headline'](plugins/os-me/lib/os_me/pattern_clusterer.rb) with no nomic task prefix ←Pair#headlineis the first AI line (plugins/os-me/lib/os_me/pair.rb) ← DESIGN: S9a (#1251) chose the headline as the cluster textVERDICT: CREATE (user-approved; backlog search only)
Work started on branch ticket-1289 (os-sdlc implement).
Resolution
Done: PatternClusterer embeds clustering: plus the moment topic (Toward value, else first non-heading non-stock line, else headline) through MomentTopic; PredictionParser#toward added; CandidateRefresh takes catalog:
Evidence: Implementation 151 complete in 6 min 12 s, 8 dispatches, all pass; suite gate green; commits
292f53beandf7a5697con ticket-1289Follow-ups: #1292 extends toward to the YAML block; live candidates rerun reported in session