Skip to content

Direct annotate gates cannot fail closed or publish an atomic result #1090

Description

@rNoz

Summary

plannotator annotate ... --gate --json emits a structured decision on stdout, but every decision exits successfully and there is no atomic no-clobber result-file contract. Automation cannot use process status as an approval gate, and detached callers must redirect stdout to a destination that can be observed while empty or partial.

Reproduction

  1. Run plannotator annotate plan.md --gate --json.
  2. Send annotations instead of approving.
  3. Observe a valid {"decision":"annotated","feedback":"..."} record on stdout.
  4. Observe that the process exits 0.
  5. Redirect stdout to a file and observe that Plannotator provides no atomic publication or no-clobber guarantee for readers.

Observed behavior

The direct JSON transport distinguishes approved, annotated, and dismissed decisions only in stdout content. Exit status remains 0, so fail-closed callers must add their own JSON parser. Shell redirection controls the destination and may expose an empty or partial file while the command is still running.

Expected behavior

Direct annotate should offer optional strict flags that require --gate --json:

  • --require-approval should exit 0 only for approval while still publishing valid annotated and dismissed JSON before a nonzero exit.
  • --result-file <path> should publish the exact newline-terminated stdout record through an atomic readiness boundary.

The result path should resolve from the invocation working directory, require an existing parent and absent destination, use a private same-directory temporary file, flush and close it, and publish with an atomic no-clobber operation. Unsupported hard links or a concurrent destination must fail closed without an overwrite or copy fallback.

This contract is atomic no-clobber publication, not crash durability: file data is synced before hard-link publication, but the parent directory is not synced.

Existing plaintext, JSON, and hook-native output must remain byte-for-byte compatible when strict flags are absent. The strict flags must not be added to shared OpenCode or Pi slash-command parsing, and --hook must remain unchanged and incompatible with them.

Root cause

Direct annotate flag handling extracts only --gate, --json, and --hook. After waitForDecision(), the command uses the legacy emitter and unconditionally exits 0. There is no separate strict serializer, exit policy, completion coordinator, or secure result-file publisher.

Impact

Foreground automation cannot distinguish requested revisions from approval using exit status alone. Detached automation has no safe readiness boundary for a result file and risks accepting stale, pre-existing, partial, or overwritten data as the current review decision.

Proposed scope

  • Add direct-only parsing and help for --require-approval and --result-file <path>.
  • Require --gate --json; reject --hook, non-annotate commands, missing values, and duplicate strict options.
  • Keep the legacy annotate emitter unchanged and use a separate strict serializer only when a strict option is present.
  • Add an injectable completion coordinator that publishes the result file, waits for stdout completion, and exits in that order.
  • Publish result files through a 0600 same-directory temporary file, file sync, close, hard link, and temporary unlink.
  • Reject existing destinations before server startup and rely on hard-link no-clobber semantics for publication races.
  • Add focused parser, serializer, coordinator, ordering, failure, and filesystem tests.
  • Document the strict direct-gate contract without changing OpenCode, Pi, or hook behavior.

Prior art and overlap

A refreshed read-only GitHub search on 2026-07-20 found no issue or pull request for --require-approval or --result-file.

Contribution

I have a tested direct-CLI implementation with focused, integrated, and live atomic-publication coverage ready to contribute. About to push the PR => Created and ready #1091

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions