fix(questions): planned_entry declares and collects its thesis capture in the same exchange (closes #667) - #692
Merged
Conversation
…e in the same exchange (closes #667) `initial_thesis`'s `planned_entry` choice declared an empty `requirements_by_choice`, so an agent following the emitted `question_opportunity.answer_contract` literally could submit the choice and still hit `_validate_thesis_completeness`'s refusal — a dead end the contract never predicted. On first_review this state is structurally unreachable honestly: every initial_thesis question exists because the cycle has no pre-existing captured thesis, so there was no earlier record to satisfy the gate with, and inventing one or spending an unbudgeted follow-up are both forbidden. Per the maintainer disposition: keep `_validate_thesis_completeness` strict (the defect is the contract, not the validator) and make `planned_entry` name what the gate will demand, reusing `new_evidence`'s existing shape (declared required field paths + non-empty requirement text) rather than inventing a parallel capture object. The capture lands in the same `thesis_updates` row every `missing_thesis_positions` entry already carries, submitted in the same `answers.json` as the `planned_entry` choice. - skills/fomo-kernel/engine/question_surface.py: `_INITIAL_THESIS_REQUIREMENTS["planned_entry"]` now names `thesis_updates.why`/`.exit_trigger`/`.maturity`, and `_requirement_copy` gives it bilingual agent-facing requirement text. - skills/fomo-kernel/engine/review.py: cross-reference comment on `_validate_thesis_completeness` pointing at the contract declaration it must stay in sync with; no behavior change. - skills/fomo-kernel/flows/first-review.md: step 3 states the one exception to the default inferred-thesis placeholder. - docs/maintainer-guide.md: new mirrored-surfaces row for the two hand-written literals (declared contract vs. enforced rule) that must stay in sync, the same drift class `new_evidence` already carries. - tests/test_question_surfaces.py: contract-drift unit test pinning the declared requirement (and that every other choice stays unrequired). - tests/test_review_v2.py: first-review fixture that reads the requirement off the real plan, supplies exactly what it names, and reaches preview; the same fixture also confirms the gate still refuses a planned_entry cycle left at maturity:"inferred". - tests/test_preview_gate.py: docstring fix — it named this exact defect as a still-current fact, which this change makes false. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
initial_thesis'splanned_entrychoice declared an emptyrequirements_by_choicein itsquestion_opportunity.answer_contract, so anagent that followed the emitted contract literally could choose
planned_entryand still hit_validate_thesis_completeness's refusal — adead end the contract never predicted. On
first_reviewthis is structurallyunreachable honestly: every
initial_thesisquestion exists because thecycle has no pre-existing captured thesis, so there is never an earlier
record to satisfy the gate with, and both escape hatches (inventing a
thesis, or spending an unbudgeted follow-up after the plan declared no
requirement) are forbidden.
Per the maintainer disposition on #667:
_validate_thesis_completenessstrict — the defect is thequestion contract, not the validator.
planned_entrynow declares its thesis-capture requirement and the agentcollects it in the same user exchange, submitted in the same
answers.jsonalongside theplanned_entrychoice.thesis_updatesrow everymissing_thesis_positionsentry alreadycarries, reusing the existing
why/exit_trigger/maturityvocabulary.new_evidence's existing shape(a list of required field paths in
requirements_by_choice, plusnon-empty
requirement_text_by_choice) rather than inventing a new one.What changed
skills/fomo-kernel/engine/question_surface.py—_INITIAL_THESIS_REQUIREMENTS["planned_entry"]now names
["thesis_updates.why", "thesis_updates.exit_trigger", "thesis_updates.maturity"]instead of
[];_requirement_copygivesplanned_entryreal bilingual(en/zh-TW) agent-facing requirement text instead of an empty string. Every
other choice (
momentum_follow/external_call/no_clear_thesis/skip)is unchanged and stays unrequired.
skills/fomo-kernel/engine/review.py— comment-only cross-reference on_validate_thesis_completenesspointing at the contract declaration itmust stay in sync with. No behavior change.
skills/fomo-kernel/flows/first-review.md— step 3 now states the oneexception to the default inferred-thesis placeholder: a
planned_entrycycle needs a real capture in the same exchange.
docs/maintainer-guide.md— new mirrored-surfaces row naming the twohand-written literals (the declared contract vs. the enforced rule) that
must stay in sync, flagged as the same drift class
new_evidence's_ADD_REQUIREMENTS/thesis.build_decision_eventspair already carries —not a new defect, but worth naming so it isn't a silent second recurrence.
tests/test_question_surfaces.py— new contract-drift unit test pinningrequirements_by_choice["planned_entry"](and that every other choice'srequirement stays
[]).tests/test_review_v2.py— new first-review fixture test that reads therequirement off the real prepared plan (not an assumption of its shape),
supplies exactly what it names, and reaches
previewsuccessfully; thesame fixture also re-confirms the gate still refuses a
planned_entrycycle left at
maturity:"inferred".tests/test_preview_gate.py— docstring fix: it explicitly named thisexact defect ("
planned_entryadvertises no requirements and still needsa captured thesis...") as a still-current fact, which this change makes
false. The test's own behavior (avoiding
planned_entryto staypayload-free) is unchanged and still correct.
Proof
tests/test_review_v2.py::test_planned_entry_capture_declared_by_the_contract_reaches_previewreads
requirements_by_choice["planned_entry"]off a real prepared plan,builds the
thesis_updatescapture from exactly those field names, andgets
previewreturn code 0._INITIAL_THESIS_REQUIREMENTS["planned_entry"]to[](the pre-fixvalue) fails
test_question_surfaces.py's new contract-drift test with:preview. (Thetest_review_v2.pyfixture alsocatches the same mutation, on its own explicit guard assertion, for the
same reason.) Mutation was applied with
cpbackup/restore,__pycache__cleared, the mutated tree import-checked clean before running tests, and
restored from the backup afterward — confirmed by
git diffshowing onlythe intended fix.
planned_entryanswer whose
thesis_updatesrow is left atmaturity:"inferred"(i.e.without the real capture) and confirms
previewstill refuses:PASS: all 49 suites passed.Test plan
python3 tests/run_all.py— all 49 suites passtest_question_surfaces.py) pins the declared contracttest_review_v2.py) follows the contractliterally on a real first-review fixture and reaches
previewbackup, suite re-confirmed green
tests/copy_corpus.py --update) — nodiff, confirming this wording lives outside the rendered-card path
🤖 Generated with Claude Code