Skip to content

fix(skillopt): local-review publish path + downgrade latch on definitive API rejections (#738)#740

Merged
jerryfane merged 1 commit into
mainfrom
fix/738-local-review-path
Jul 7, 2026
Merged

fix(skillopt): local-review publish path + downgrade latch on definitive API rejections (#738)#740
jerryfane merged 1 commit into
mainfrom
fix/738-local-review-path

Conversation

@jerryfane

Copy link
Copy Markdown
Owner

Closes #738

Problem

A live training session wedged: its human feedback was already TrainingReady (ranked + pairwise imported via feedback markdown import), but train continue could not reach review_published without a successful GitHub post — and the post 422'd because the review packet exceeds GitHub's 65,536-char issue-body limit. Two root causes.

1. Local-review path in the state machine

publishSkillOptTrainReview now checks loadSkillOptReviewStatus for the iteration's eval run before attempting the GitHub post. If status.TrainingReady is already true, it records the review against the local surface (metadata {status: published, review_surface: local}), registers no GitHub watch, transitions to review_published, and returns a LocalSurface result. The next train continue hop takes the existing TrainingReady -> feedback_synced path.

  • The check runs before recoverSkillOptTrainReviewPublication, so it also unwedges a session left with a stale posting_external marker.
  • A pending published_external recovery marker (a real GitHub post that only needs a state-resume) is still honored via recover, not shadowed by a local record.
  • When feedback is not yet ready, behavior is byte-identical: GitHub stays the default surface (publish-first / sync-skipped-after preserved).

2. Latch downgrade for definitive rejections

Extends #735's downgradeReviewMarkerOnLaunchFailure: a definitive 4xx API rejection (HTTP 422 validation / 404 / 403) — which proves no issue was created — now also downgrades the posting_external marker (status failed_external_rejected) so the retry proceeds. Ambiguous failures (timeouts, kills, 5xx, socket drops) keep the conservative latch. Grounded in a new github.IsDefinitiveRejectionMessage classifier that sits alongside IsTransientMessage (disjoint 4xx vs 5xx/transport signature sets).

3. Size-aware degradation (nice-to-have)

Left as a code TODO(#738 part 3) at the publish site — non-trivial (summary issue + trailing comments/artifact). Parts 1+2 already resolve the wedge for the reported scenario.

Tests (deterministic, no network)

  • TestSkillOptTrainContinueLocalReviewSkipsGitHub — TrainingReady-before-publish → continue skips GitHub (no client constructed), metadata carries review_surface: local, state → review_published, no watch registered.
  • TestSkillOptTrainContinuePublishesToGitHubWhenFeedbackNotReady — not-ready → GitHub publish path invoked exactly as before (regression).
  • TestDowngradeReviewMarkerOnDefinitiveRejection — 422/404/403 downgrade + retry proceeds; 5xx/timeout/killed/socket preserve the latch.
  • TestSkillOptTrainContinueLocalReviewEndToEnd — full local path: seeded options_generated with imported TrainingReady feedback → two continue hops reach feedback_synced with zero GitHub calls.
  • TestIsDefinitiveRejectionMessage (internal/github) — classifier unit coverage.

Gate

  • go build ./... OK
  • go vet ./... OK
  • go test -count=1 ./internal/cli/ OK (211.99s)
  • go test -count=1 ./internal/github/ OK; go test -race ./internal/github/ OK
  • scoped -race on the new cli tests OK

🤖 Generated with Claude Code

…ive rejections

Closes #738

When a train session's human feedback is already imported locally
(TrainingReady) before the review-publish step, `train continue` now records
the review against the local markdown surface and advances to review_published
without any GitHub call or watch registration, then lets the next hop take the
existing TrainingReady -> feedback_synced path. Publish is a review SURFACE and
the local markdown surface is already first-class for import, so a doomed GitHub
issue (packet over GitHub's 64KB issue-body limit) is skipped entirely. When
feedback is not yet ready, GitHub stays the default publish surface
(byte-identical), including the June-validated publish-first / sync-skipped-after
behavior.

Also extends #735's posting_external marker downgrade so a definitive 4xx API
rejection (HTTP 422 validation / 404 / 403) — which proves no issue was created —
clears the latch and lets the retry proceed, while ambiguous failures (timeouts,
kills, 5xx, socket drops) keep the conservative latch. Grounded in a new
github.IsDefinitiveRejectionMessage classifier alongside IsTransientMessage.

Part 3 (size-aware publish degradation) left as a code TODO referencing #738.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jerryfane jerryfane merged commit 8b4a0d8 into main Jul 7, 2026
1 check passed
@jerryfane jerryfane deleted the fix/738-local-review-path branch July 7, 2026 23:46
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.

skillopt train: review publish 422s on GitHub's 64KB issue-body limit + no local-review path in the state machine

1 participant