feat: reject an under-asked `answer_commit` once, naming the missing item
Issue for this PR
No linked issue (feature PR).
Type of change
- Bug fix
- New feature
- Refactor / code improvement
- Documentation
What does this PR do?
The analyst sometimes answers a two-part request ("which repository and its most copied file") with a commit that has one field. With answer_only, the second item then never reaches the reply. This ports a check already proven on the MCP surface to native answer_commit.
One-time conjunction check
- New module
answer-commit-conjunction.ts(module scope) estimates how many items the request asks for. It counts connectives such as "and its/their", "along with", "as well as", ", and" and "and the/which/...". It reads the first paragraph only and takes the sentence with the most items. It ignores ranges ("between X and Y", "both/either/from ... and"), constraint clauses ("and only", "and excluding", ...) and rounding-only sentences. - A fields commit with fewer fields than that count is rejected once. The error lists the committed field names and quotes the clause that introduces each extra item (for example "and its most copied file"), so the model knows what is missing.
- An unchanged recommit is accepted. "Once" is per session and request: an in-process set plus a history scan for the rejection marker (covers a restart). A new request is checked again.
- Rows and unresolved commits are never checked. The check never fires near the time budget (query deadline passed, effort downgraded, or tools disabled for finalization), or when the latest user message is synthetic.
- Purely lexical: no queries, no model calls, no tool schema change.
Where the request text comes from. The tool reads the session's latest non-synthetic user message from ctx.messages, which the prompt loop already passes to every tool. The MCP surface needed a model-supplied request argument only because an MCP server never sees the user's message; native does not need one.
Answer layout (unchanged on purpose). On MCP, the proven change put the basis above the FINAL ANSWER line for complete commits and kept it on that line for under-asked ones. Native already renders the basis as its own paragraph above the **Answer:** line for every commit, and the reply is the whole committed text. So an under-asked commit's basis is still in the reply, and a complete commit's answer line has no basis. Remaining differences: native keeps field labels and the answer label on that line, and answer_only drops the basis on both surfaces. Tests pin this layout.
Earlier drafts of this PR added an optional asked field and extra detector exclusions (plural heads, same-entity criteria). Both are removed: the proven design counts committed fields only and uses the detector as shipped on MCP.
How did you verify your code works?
answer-commit-conjunction.test.ts(26 tests): detector counts and quoted clauses; one rejection naming the missing item; unchanged recommit accepted (in-process and from history); parallel commits; new request re-checked; 2-of-3 fields rejected; rows, unresolved and complete commits accepted; near-deadline skips; answer layout.prompt.test.ts: a new case runs the real prompt loop with a fake model. The one-field commit for a two-item question is rejected, the model sees it, the unchanged recommit completes, and the reply equals the committed text.- Both fail on origin/main in a detached temp worktree.
- Every test file referencing the changed modules plus both tool-schema suites: 455 pass, 0 fail.
bun run typecheckand the strict upstream marker check pass.- Local read-only Codex review of head 215b068c4c9ffd084519f94df19ba07d0a26c405: no P0/P1. An independent review also ran. Fixed from it: the rejection now sets failure_kind conjunction_check so telemetry can tell it apart from real commit failures, and the synthetic-reminder test now fails if the synthetic filter is removed.
Known limits (all fail open, so no worse than today): after a loop-written synthetic user message (compaction continue, task summary, validator retry), the check reads an empty request and does not fire. An identical request repeated later in the same process is not checked again. After compaction plus a restart, one more rejection is possible. Broad "and the ..." phrases can count a filter as an item, which costs one recommit turn. This is the detector as proven, kept on purpose.
Cost side effects
None. The check is a regex over the user's message. A rejected commit costs at most one extra model turn per request.
Screenshots / recordings
Not a UI change.
Checklist
- I have tested my changes locally
- I have not included unrelated changes in this PR
- I checked for cost side effects (model calls, queries or cloud resources that grow with user data) and described them, or wrote "none"
🤖 Generated with Claude Code
Summary by CodeRabbit
- Bug Fixes
- Answer submissions with fewer fields than the latest request asks for are flagged, with the missing request details included. An unchanged resubmission can still be accepted.
- Validation is skipped when the time budget is nearly exhausted or a completion reminder is present, allowing the answer to be committed in those cases.
- Validation now applies only once per request and session, reducing repeated rejection messages while preserving the ability to submit a corrected answer.
[!NOTE]
Medium Risk
Changes analyst terminal commit behavior and can add an extra model turn on false positives, but failures are advisory, once-per-request, and skipped when the run is near budget.
Overview
Adds a one-time lexical guard on native answer_commit fields commits: when the latest non-synthetic user message looks like it asks for multiple items (via patterns such as “and its…”, “along with”, “, and”), a commit with fewer fields is rejected once with quoted missing clauses; the next commit is accepted even if unchanged. The check reads ctx.messages, skips rows/unresolved payloads, and is disabled near run time budget; rejections use failure_kind: conjunction_check for telemetry.
Coverage includes unit tests for the detector and tool behavior, a prompt-loop integration test, and a small warehouse-list test fix that isolates the shared global connections registry file between cases.
Reviewed by Cursor Bugbot for commit 2dfb4838d28b381653d2caaf34c6f3a8a5dd0646. Bugbot is set up for automated code reviews on this repo. Configure here.