Skip to content

ship-status advertises check --commit from accepted, where check's own state machine rejects it #690

Description

@0xLeif

Measured on a real repository. The tool computes "your next step is X" from a state in which X is refused by its own state machine.

Observed

$ specsync change check give-every-baby-jay-a-page-... --commit
Checking (1/2): verifying the materialized tree…
error: cannot check the change while give-every-baby-jay-a-page-... is accepted;
       expected approved or implementing or verifying

ship-status at the same moment, unchanged, still advertising exactly that command:

give-every-baby-jay-a-page-...  accepted  tip=product  (not ready)
  Verification: c99ebc7d (not ancestor of HEAD)
  Stages:
    - [current] product_tip: run `specsync change check <id> --commit`, push the product tip, ...
    - [done]    review_tip:  scoped review is recorded
    - [pending] archive_tip: complete product tip and independent review first

Mechanism

src/commands/change.rs:1171-1188:

let verified = verification_present && verification_ancestor && verification_commit.is_some();

let product_done = verified
    && matches!(record.state, Verifying | Accepted | Archived);

// when !product_done:
format!("run `specsync change check {id} --commit`, push the product tip, ...")

And check's guard, src/change.rs:6852:

ChangeState::Approved | ChangeState::Implementing | ChangeState::Verifying

The stage's done predicate admits Accepted. The action printed when not done is valid only for states that exclude Accepted.

So state == Accepted with verification_ancestor == false — which is what a squash-merge produces (#689) — lands in the gap: not done, therefore print check --commit; but check refuses that state.

Why the state reaches that combination at all

Squash-merge rewrites the verification commit, so verification_ancestor goes false while the record stays accepted. See #689 — on a squash-only repository there is no merge path that avoids it.

Where it leaves the change

state:      accepted
evidence:   verification commit not an ancestor of HEAD (squashed)
re-verify:  refused — wrong state
archive:    refused — post-move preflight on a coupled predecessor (#688)
only door:  specsync change reopen --actor <human> --reason <text>

Wedged by two rules whose escapes each require the other side to move.

Suggested fix

The reporter's framing, which I think is right and is a small extension of the disclosure fix already proposed in #688:

When the computed next step is not reachable from the current state, say so and name the transition that is. "You are in accepted; check requires verifying; run reopen to return there" is strictly more useful than printing a command that errors.

The tool has everything needed: it knows the record's state, and each verb's accepted states are already explicit in the guards. A stage action that cannot execute from the current state should never be printed as the current stage.

A design note in the tool's favour, from the reporter

reopen requires --actor and --reason and describes itself as an audited transition. That is the right shape for the only door out of a wedged state — and it means the door cannot be opened by an agent acting alone. The reporting session declined to run it on exactly that basis: an audited human-authorization flag is not something an agent should supply on a user's behalf.

Worth preserving that property in any fix here. The goal is to name the door, not to make it easier to walk through unattended.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:product-bugRuntime or CLI behavior is incorrectarea:uxMessages, output, or next actions mislead or frustrateimpact:developer-experienceAffects authors, maintainers, or local workflowsimpact:user-facingCan affect an end user or repository operator directlypriority:p2Important but not immediately release-blockingresolution:partialRelated work landed but meaningful residual work remainsscope:specsync-6Applies to current SpecSync 6 behavior or release contract

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions