refactor: single member_turn_context chokepoint for rules currency (#8609) - #8864
Conversation
…8609) The 'every member turn runs under current rules' invariant was stitched across five hand-coordinated branches: the lifecycle table in build_message, the chat_runner finally-block re-arm, and the PUT rules handler's hand-built dashboard:{member_slot_key(slug)} key. No single chokepoint owned it, so a future session-lifecycle branch added to build_message could silently skip both the member section and the fail-closed rules gate. Extract the decision into kiro_crew.members: - MemberLifecycle: the five session-lifecycle states, derived by member_lifecycle() from the same inputs build_message branches on, with delivers_section exposing the delivery half of the verdict. - member_turn_context(member, lifecycle): THE decision point — every lifecycle either delivers the current section (whose builder reads rules fail-closed) or runs the standalone rules gate; MINIMAL is the pinned deliberate exception (never a member thread by contract). - member_thread_session_alias(slug): the canonical dashboard session alias, replacing the two hand-built f-strings in the members handler. All delivery branches now consult the chokepoint: the four build_message/build_session_context sites test deliver_section / enforce_rules_gate, the chat_runner pending flag reads MemberLifecycle.delivers_section (mode-keyed — the member name resolves later, at the context build), and the rules-write reinjection flag derives its key from the alias helper. Zero behavior change: the existing member-prompt suites (~270 tests, early-exit re-arm paths included) pass unchanged. New tests pin the verdict truth table, the exhaustive lifecycle mapping, the rules-currency invariant itself, and — via AST wiring guards in the style of the SEL offload guard — that every delivery branch routes through the chokepoint. Closes #8609
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS A comment-table invariant becomes one test-pinned chokepoint; I verified all five replaced predicates are equivalent on every reachable input, so the extraction is genuinely behavior-preserving. Suggestions
[DESIGN-REVIEWED] 2d7e06e |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — 🟡 CONCERNSPremise-level review of All verification is complete. The change is a design-review-deferred, cause-level refactor with every declared item present; the two real findings are zero-consumer internal surface (the First-Principles-Verdict: CONCERNS Cause-level fix with a recorded provenance (#8609), but two pieces of the new surface have zero consumers: the What this change shipsIntent: make "every member turn runs under current rules" enforceable by one testable function instead of a comment table — a FIX (deferred Design Review finding, recorded in issue #8609, so the requirement is derived).
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 2d7e06e |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThis is a behavior-preserving refactor consolidating member-lifecycle branching into the
The No findings. [OPUS-REVIEWED] 2d7e06e Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
|
Answering the First Principles CONCERNS (advisory; both subtractions acknowledged individually):
|
bolichen97
left a comment
There was a problem hiding this comment.
Tech Lead review: APPROVE.
Verified behavior preservation at every converted predicate against the head source, not just the reviewer summaries:
context.py:2704(FRESH leg) -member_turn_context(member, FRESH).deliver_sectionreduces tobool(member)becauseFRESHis indelivers_section, so the hard-coded lifecycle is equivalent for any caller ofbuild_session_context, independent of whether the minimal early-return at 2572 dominates it.context.py:3161(rules gate) -enforce_rules_gate=member and lifecycle is WARM, andmember_lifecycleyieldsWARMexactly whennot is_new_session and not needs_reinjection. Identical to the removed gate.context.py:3186(slim_resume) - sits insideif is_new_session:(line 3170), wherelifecycle is SLIM_RESUME<=>resumed and not minimal_context. Identical inside that branch.context.py:3290- insideif slim_resume:(3266), so lifecycle isSLIM_RESUME, which delivers; reduces toif member.context.py:3407- insideif not is_new_session and needs_reinjection:(3365), so lifecycle isWARM_REINJECTION, which delivers; reduces toif member.chat_runner.py:6547- withminimal_context=False, needs_reinjection=False,delivers_sectionis True exactly whenis_new(FRESH and SLIM_RESUME both deliver, WARM does not), so it equals the removedbool(slot.mode == "member" and is_new).
All three _build_member_section call sites are converted; none were left branching by hand.
member_thread_session_alias extraction is also equivalence-safe: a non-empty row["slot_key"] can only come from read_dm_binding, which rejects any binding whose slot_key is not member_slot_key(slug) (members.py:528), so the substitution yields the same string. It cannot raise a new MemberSlugError either, since read_dm_binding already called member_slot_key(slug) unguarded on the path that produced the row.
CI: 57 success / 6 skipped / 0 failure on the head sha, 0 annotations anywhere (the single Focus Cue annotation is a notice-level report about 5 pre-existing untouched elements). All five AI lanes non-blocking; First Principles CONCERNS is advisory and both subtractions were answered by the author. The new AST wiring test pins the chokepoint so a future delivery branch cannot re-hand-roll the predicate.
Problem / Motivation
Design Review follow-up from PR #7235 (member system prompt for crew DM threads), deferred to #8609 while that PR was at the merge-ready point. The "every member turn runs under current rules" invariant was stitched across five hand-coordinated branches: the lifecycle table in
build_message, the fresh-session injection inbuild_session_context, thechat_runnerfinally-block re-arm, and the PUT rules handler'smark_needs_reinjectionwith a hand-builtdashboard:{member_slot_key(slug)}key. No single chokepoint owned the invariant, so a future session-lifecycle branch added tobuild_messagecould silently skip both the member section and the fail-closed rules gate.Why it matters
The rules layer is the user's safety boundary for a crew member. A lifecycle branch that bypasses both delivery and the rules gate would run the member with no
[PERMANENT RULES]at all, silently — and nothing in the old shape would fail: the coordination lived in a comment table, which no test can pin.What changed (motivation → approach → change)
Goal: make the invariant live in one function a test can pin, exactly as the issue specifies. Approach: extract the decision, not the actions — each branch keeps its own injection mechanics and consults a shared verdict, keeping the diff behavior-preserving.
New in
kiro_crew.members:MemberLifecycle— the five session-lifecycle states (FRESH,SLIM_RESUME,WARM_REINJECTION,WARM,MINIMAL), derived bymember_lifecycle()from the same inputsbuild_messagealready branches on, withdelivers_sectionexposing the delivery half of the verdict.member_turn_context(member, lifecycle)— THE decision point: every lifecycle either delivers the current section (whose builder reads rules fail-closed) or runs the standalone rules gate;MINIMALis the pinned deliberate exception (never a member thread by contract). Fails closed on a non-enum lifecycle (TypeError) — the str mixin makes a bare"warm"compare equal to the member while failing the identity tests, which would otherwise yield the one combination the invariant forbids.member_thread_session_alias(slug)— the canonicaldashboard:<slot key>alias, replacing the three hand-built f-strings in the members handler (rules-write reinjection flag, thread-open history probe, roster transcript-tail probe).All delivery branches now route through the chokepoint: the gate and both re-injection sites in
build_messagetestenforce_rules_gate/deliver_section, thebuild_session_contextfresh-session site consults theFRESHverdict,slim_resumeis derived from the chokepoint's lifecycle rather than re-encoding the resume predicate, and thechat_runnerpending flag readsMemberLifecycle.delivers_section(mode-keyed — the member name resolves later, at the context build).Zero behavior change: every replaced predicate is equivalent to the original on all reachable input combinations.
Tests
New module
test/test_member_turn_context.py:delivers_sectionagrees with the verdict.MINIMALlifecycle takes exactly one of the two mechanisms.member_lifecyclemapping (including theminimal_context-beats-resumedprecedence) and full-enum reachability."warm") raisesTypeError.is_member_session_key, bad-slug refusal._build_member_sectioncall inbuild_message/build_session_contextsits under adeliver_sectiontest, the standalone rules read sits underenforce_rules_gate, the lifecycle is derived frommember_lifecyclewith the real branch inputs,slim_resumereads the chokepoint's lifecycle, the chat_runner flag routes throughdelivers_section, and no f-string in the members handler carries adashboard:constant.Existing member-prompt suites (~270 tests, early-exit re-arm paths included) pass unchanged — no existing test was edited.
Manual verification
N/A — unit coverage sufficient: the change is a behavior-preserving extraction pinned by the existing behavioral suites plus the new structural guards; no user-visible surface changed.
Related Issues
Closes #8609
Pattern harvest
Rule candidate: review-prompt
Pattern: an invariant coordinated across branches by a comment table instead of a callable chokepoint a test can pin
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)