Skip to content

fix(rules): stop reporting undelivered requests as success, and poll to a deadline - #660

Merged
theCodeDrift merged 3 commits into
mainfrom
fix/rule-request-status
Nov 9, 2026
Merged

theCodeDrift merged 3 commits into
mainfrom
fix/rule-request-status

Conversation

@theCodeDrift

Copy link
Copy Markdown
Member

Two fixes to how rule create / rule improve wait on a generation request.

#648: an end state that delivers nothing no longer exits 0

completeRequest's default branch printed the success envelope with rules: [] and returned 0 for pr, merged, closed, an unknown status, or a missing one. A --json caller could not tell "nothing was delivered" from "it worked and produced nothing".

Those states now go through options.fail with RULE_GENERATION_FAILED and a message naming the state (packages/cli/src/commands/rules.ts, undeliverableMessage). pr/merged/closed get their own wording: the rules went to a pull request, not to this CLI, which is reachable through --resume on an older request. The code is reused rather than added, since a request that ends without delivering rules is what it already covers. completeRequest now prints the success envelope only for generated.

#638: pushing, unknown statuses, and an overall deadline

  • Only explicitly terminal statuses (generated, failed, unsupported, pr, merged, closed) end the poll. pushing is in progress with its own line ("Status: pushing — pushing the rule to a pull request..."), and any status the CLI does not know keeps polling. pushing is handled as a plain string in generate.ts; the generated schema/types are untouched, since they come from the live schema.
  • A response with no status is a malformed body and fails at once with NETWORK_ERROR, rather than being waited on.
  • New deadline. awaitRequest was while (true), ended only by MAX_CONSECUTIVE_UNAVAILABLE. It now gives up after REQUEST_DEADLINE_MS = 30 minutes with NETWORK_ERROR, the last status seen, and the --resume command (the existing give-up wording and code, which the recipes already route to --resume). The CLI had no poll timeout to reuse, so the value comes from the service: the generator resets a request stuck in building/pushing for over 15 minutes to failed (taskless/taskless openspec/specs/ticket-pipeline/spec.md). Twice that leaves room for queue time and for that reset to be reported, so the deadline only catches what the service never resolves. The deadline and clock are injectable through GenerationContext (deadlineMs, now), following auth/login-step.ts.

Tests

packages/cli/test/rule-poll-transient.test.ts, driving the real command: pr/merged/closed and a missing status fail non-zero with an error envelope; an unknown status that never ends fails at the deadline after 120 polls; pushing and an unknown status are waited through and then delivered; a request stuck in building gives up at the deadline. The tests advance a fake clock in the stubbed setTimeout, so nothing waits in real time. All of them fail before the fix.

Fixes #648
Fixes #638

A request that ended in pr, merged, closed, an unknown status or no status
printed the success envelope with rules: [] and exited 0. It now fails with
RULE_GENERATION_FAILED and a message naming the state.
The request poll treated every status but accepted, classifying and building
as final, so the server's pushing step, or any status added later, ended the
wait early. Only explicitly terminal statuses now end the poll; pushing gets
its own progress line and unknown statuses keep polling.

awaitRequest also had no overall timeout. It now gives up after
REQUEST_DEADLINE_MS (30 minutes, twice the generator's 15-minute stale-build
reset) with NETWORK_ERROR and the --resume command. The deadline and clock are
injectable through GenerationContext.
@theCodeDrift

Copy link
Copy Markdown
Member Author

@claude /review

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @theCodeDrift's task in 3m 58s —— View job


Review: fix(rules): stop reporting undelivered requests as success, and poll to a deadline

Review mode: incremental — read 0 prior review thread(s) before reviewing. .prior-review.json held only the trigger comment itself (no reviews, no review threads, no other top-level comments), so this is the first review pass on this PR.

  • Read .prior-review.json
  • Gather PR diff and changed files
  • Review packages/cli/src/commands/rules.ts
  • Review packages/cli/src/rules/generate.ts
  • Review tests (rule-poll-transient.test.ts, support/v2-server.ts)
  • Review changeset
  • Post final review

Correctness. I traced awaitRequest's new control flow end to end: TERMINAL_STATUSES (generated/failed/unsupported/pr/merged/closed) exactly matches the published status enum in src/generated/api-v2.d.ts, and pushing/unknown strings correctly fall through to the progress branch rather than ending the poll. The deadline math checks out — deadline = now() + deadlineMs, checked before each pause(), gives exactly 120 polls for the default 30-minute/15-second combination, matching the test's assertion. The malformed-body guard (typeof state !== "string" || state === "") correctly fails fast instead of waiting on garbage. In rules.ts, completeRequest's default branch can only now be reached for pr/merged/closed (the other two undeliverableMessage branches — missing status, unrecognized string — are dead given awaitRequest's own guards), but that's acknowledged in the function's own comment as deliberate defense-in-depth, consistent with this codebase's style elsewhere.

Tests. Good coverage of the new behavior: pr/merged/closed failing with the pull-request wording, a never-ending unknown status hitting the deadline at exactly 120 polls, a missing status failing without printing "undefined", pushing and an unknown status being waited through and delivered, and a stuck building request giving up at the deadline. The fake-clock/fake-setTimeout harness is correctly wired to advance in lockstep with POLL_INTERVAL_MS.

New findings:

  1. (posted inline on packages/cli/src/rules/generate.ts) The deadlineMs doc comment says "tests shorten it," but no caller or test actually sets context.deadlineMs/context.now — rules.ts never populates them, and the test suite shortens the effective wait by mocking global Date.now/setTimeout instead. The fields are real and the defaults work correctly, but the comment describes a test path that doesn't exist, and the injection point itself is currently unexercised.

  2. Spec/code divergence (could not post inline — file isn't part of the diff): openspec/specs/cli-rules/spec.md (lines 86–108, and the polling requirements around line 149–197) still documents polling as ending at generated/failed/unsupported only, with no deadline. This PR changes that: pr/merged/closed now end the poll and fail, pushing is a recognized progress state, and there's a new 30-minute give-up. Since this repo treats spec.md as the normative description of CLI behavior (and changes are normally proposed via openspec/changes/), this looks like a real gap introduced by this PR rather than pre-existing drift — worth a follow-up spec update even if out of scope for this diff.

Nothing else stood out — the undeliverableMessage/TERMINAL_STATUSES/PROGRESS refactor is a clean, well-reasoned simplification of the old switch, and the changeset wording accurately describes the behavior change.

Comment thread packages/cli/src/rules/generate.ts Resolved
awaitRequest's deadlineMs and now were documented as test seams but no test
used them. Add one that calls awaitRequest with both, and fails if the
injected clock is ignored.
@theCodeDrift
theCodeDrift merged commit a7bc678 into main Nov 9, 2026
5 checks passed
@theCodeDrift
theCodeDrift deleted the fix/rule-request-status branch November 9, 2026 17:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant