fix(sdk,cli): terminal-state no-ops, bounded pm next receipts, lean refusals with typed claim holders
Closed
UN
2d ago+1532+1532+0123456789012345678901234567890123456789-98-98-01234567890123456789
Three GitHub-reported agent-UX and SDK defects, each reproduced on current source, fixed with a measured before/after and a negative control, and closed in the tracker with escape class and gate evidence.
What changes
- Identical terminal updates keep their closure clocks (GH-1628, pm-gh1628).
updaterouted any payload naming the close status to the closure writer without checking whether the item was already closed, so a forced identical closure re-stampedclosed_atandupdated_aton every retry while the same payload on an open item was a no-op.routeCloseStatusUpdatenow reads the stored item first: when the requested close status, trimmed reason andcompleted_atalready describe it, only the remaining fields take the ordinary skip-no-op update path and the result reportsterminal_state_unchanged; a bare identical update returns no changed fields without--force. A different reason is still a repeat closure behind the existing force gate.commitItemMutationsreusesupdate(), so transactions inherit the fix. Helpers live insrc/sdk/lifecycle/repeated-terminal-state.tsto keepupdate.tsunder the line ceiling. pm nextdiscloses the whole response under its ceiling and encodes tags losslessly (GH-1627, pm-gh1627).filters.token_budgetreported 640 while nothing enforced it, and--token-budgetreplaced the universal default ceiling instead of bounding the selection beside it, so the complete response carried no cost receipt. The flag is no longer a read-output cost alias; a requested selection budget now audits the complete envelope (read_outputwithbudget_source: default,budget_tokens,estimated_tokensbesidetruncation.ready_budget) unless an enforced intent receipt already discloses it, andfilters.token_budgetechoes the request ornull. The TOON codec records omitted null columns asnull_column_indexinstead of repeating the fullcolumn_orderlist (decoders still accept the legacy field), so the compact table wins forreadyandcontainersand every section list-encodes tags. On the real tracker: 9,148 → 8,718 bytes, 182 → 76 escaped-quote sequences.- Lean refusals drop constant boilerplate and the claim holder is typed (GH-1610, pm-gh1610). The SDK declares the constant refusal boilerplate once (
PM_GENERIC_REFUSAL_GUIDANCE,PM_RECOVERY_ECHO_FIELDS,isGenericRefusal*,projectLeanRecoveryPayload) and both lean projections consume it: lean text keeps the headline, next steps, specific requirement/rationale, verifier findings and specific recovery; lean JSON drops genericrequired/examplesand the invocation echo on top oftitle/why. A single-line message is no longer repeated under What happened in any mode.already_claimed_bycarriesitem_id,claimed_by,claimed_atandclaim_sourcethroughPmCliErrorContext, the JSON envelope and the classification; text rendersHeld by: <holder> (<source> since <time>). Claim conflict: text 798 → 719 bytes, lean text 813 → 494, lean JSON 968 → 648. A property test over six refusal codes asserts lean is never larger than default and shares no constant line.
Contracts and docs
- SDK surface snapshot acknowledges
NextResult.filters.token_budget(number | null),PmTableCellEncoding.null_column_index, thePmCliErrorContextholder fields and the new exports (resolveItemOwnershipConflict, the lean boilerplate helpers,CloseRouteContext). - Contracts snapshot drops the stale
--output-budgetalias declaration onnext. docs/COMMANDS.md,docs/AGENT_OUTPUT_ENCODINGS.md,docs/AGENT_UX_CONTRACTS.mdanddocs/AGENT_GUIDE.mddescribe the terminal-state receipt, the twonextbudget scopes, the lean refusal contract and the holder fields.
Validation
node scripts/run-tests.mjs test: full fast suite green on the merged branch (see the last commit's evidence in the tracker).- Negative controls: with the relevant source files stashed, 3 of 4 GH-1628 tests, 3 of 3 GH-1627 tests and the GH-1610 size property fail.
- Gates run locally: static quality (complexity, line ceiling, docstrings), token-budget, context-intent calibration, agent-task-token, refusal-closure, docs-skills, defect-evidence, graph-composition (prose-edge-gaps at its ceiling), record-integrity, tracker-context-quality, flag-lexicon, flag-invocation-parity, command-grammar, surface-replication.
- Per-item measurements, linked tests and gate evidence are recorded on pm-gh1628, pm-gh1627 and pm-gh1610.
Closes #1628
Closes #1627
Closes #1610
Summary by Sourcery
Fix terminal-state update retries, clarify and audit pm next budgets, and streamline refusal diagnostics with structured claim ownership data.
New Features:
- Expose structured ownership-holder details in claim conflict errors and SDK APIs.
- Add compact, lossless TOON encoding for omitted null columns and typed lean-refusal projections.
Bug Fixes:
- Preserve closure timestamps and history when an update repeats an item's existing terminal state.
- Make
pm nexttreat--token-budgetas a selection limit while auditing the complete response under the universal output ceiling. - Reduce refusal output duplication and retain actionable recovery and holder information in lean text and JSON modes.
Enhancements:
- Document terminal-state no-op behavior,
pm nextbudget scopes, lean refusal output, and typed claim-holder fields. - Extend public SDK and output contracts for ownership conflicts, refusal helpers, next budget metadata, and table encoding.
Documentation:
- Update command and agent UX documentation for terminal-state receipts, budget accounting, lossless table encoding, lean refusals, and claim ownership details.
Tests:
- Add coverage for repeated terminal updates, transactional retries, bounded
pm nextreceipts, compact table round-tripping, typed claim conflicts, and lean refusal size guarantees.
Chores:
- Record the three defect fixes in the changelog and tracker history.