Skip to content

fix(questions): planned_entry declares and collects its thesis capture in the same exchange (closes #667) - #692

Merged
atomchung merged 1 commit into
mainfrom
claude/667-planned-entry-capture
Jul 31, 2026
Merged

fix(questions): planned_entry declares and collects its thesis capture in the same exchange (closes #667)#692
atomchung merged 1 commit into
mainfrom
claude/667-planned-entry-capture

Conversation

@atomchung

Copy link
Copy Markdown
Owner

Summary

initial_thesis's planned_entry choice declared an empty
requirements_by_choice in its question_opportunity.answer_contract, so an
agent that followed the emitted contract literally could choose
planned_entry and still hit _validate_thesis_completeness's refusal — a
dead end the contract never predicted. On first_review this is structurally
unreachable honestly: every initial_thesis question exists because the
cycle 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:

  • Keep _validate_thesis_completeness strict — the defect is the
    question contract, not the validator.
  • planned_entry now declares its thesis-capture requirement and the agent
    collects it in the same user exchange, submitted in the same
    answers.json alongside the planned_entry choice.
  • No parallel capture object: the requirement resolves into the existing
    thesis_updates row every missing_thesis_positions entry already
    carries, reusing the existing why/exit_trigger/maturity vocabulary.
  • The declaration mechanism itself reuses new_evidence's existing shape
    (a list of required field paths in requirements_by_choice, plus
    non-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_copy gives planned_entry real 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_completeness pointing at the contract declaration it
    must stay in sync with. No behavior change.
  • skills/fomo-kernel/flows/first-review.md — step 3 now states the one
    exception to the default inferred-thesis placeholder: a planned_entry
    cycle needs a real capture in the same exchange.
  • docs/maintainer-guide.md — new mirrored-surfaces row naming the two
    hand-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_events pair 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 pinning
    requirements_by_choice["planned_entry"] (and that every other choice's
    requirement stays []).
  • tests/test_review_v2.py — new first-review fixture test that reads the
    requirement off the real prepared plan (not an assumption of its shape),
    supplies exactly what it names, and reaches preview successfully; the
    same fixture also re-confirms the gate still refuses a planned_entry
    cycle left at maturity:"inferred".
  • tests/test_preview_gate.py — docstring fix: it explicitly named this
    exact defect ("planned_entry advertises no requirements and still needs
    a captured thesis...") as a still-current fact, which this change makes
    false. The test's own behavior (avoiding planned_entry to stay
    payload-free) is unchanged and still correct.

Proof

  1. Contract followed literally reaches preview. tests/test_review_v2.py::test_planned_entry_capture_declared_by_the_contract_reaches_preview
    reads requirements_by_choice["planned_entry"] off a real prepared plan,
    builds the thesis_updates capture from exactly those field names, and
    gets preview return code 0.
  2. Mutation reddens at the contract level. Restoring
    _INITIAL_THESIS_REQUIREMENTS["planned_entry"] to [] (the pre-fix
    value) fails test_question_surfaces.py's new contract-drift test with:
    AssertionError
      File ".../tests/test_question_surfaces.py", line 175, in test_planned_entry_declares_its_thesis_capture_requirement_and_others_stay_empty
        assert contract["requirements_by_choice"]["planned_entry"] == \
               ["thesis_updates.why", "thesis_updates.exit_trigger", "thesis_updates.maturity"]
    
    — before ever reaching preview. (The test_review_v2.py fixture also
    catches the same mutation, on its own explicit guard assertion, for the
    same reason.) Mutation was applied with cp backup/restore, __pycache__
    cleared, the mutated tree import-checked clean before running tests, and
    restored from the backup afterward — confirmed by git diff showing only
    the intended fix.
  3. Validator stays strict. The same fixture submits a planned_entry
    answer whose thesis_updates row is left at maturity:"inferred" (i.e.
    without the real capture) and confirms preview still refuses:
    {"error": "planned_entry initial-thesis answers require a captured thesis (maturity draft or testable) for cycles: AAA#2026-01-01#1", "status": "error"}
    
  4. Full offline suite green: PASS: all 49 suites passed.

Test plan

  • python3 tests/run_all.py — all 49 suites pass
  • New unit test (test_question_surfaces.py) pins the declared contract
  • New integration test (test_review_v2.py) follows the contract
    literally on a real first-review fixture and reaches preview
  • Mutation dance run for real, red output captured above, restored from
    backup, suite re-confirmed green
  • Copy corpus golden regenerated (tests/copy_corpus.py --update) — no
    diff, confirming this wording lives outside the rendered-card path

🤖 Generated with Claude Code

…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>
@atomchung
atomchung marked this pull request as ready for review July 31, 2026 17:45
@atomchung
atomchung merged commit 843c085 into main Jul 31, 2026
3 checks passed
@atomchung
atomchung deleted the claude/667-planned-entry-capture branch July 31, 2026 20:08
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.

1 participant