Skip to content

Commit b928837

Browse files
mikemolinetclaude
andcommitted
feat(fire): add --exit-criteria + --idempotency-key flags (cueapi #632 + #683 parity)
Extends the existing fire command with two flags covering recently-shipped server features. --send-at was already there from a prior port (#618). --exit-criteria (repeatable, max 20) §14 work-verification-light required-assertion keys (cueapi #632). Receiver MUST report values for every key under outcome.assertions; missing keys mark the execution verification_failed. Pass empty string '' once to opt out of cue-level required_assertions for this fire (server distinguishes [] = explicit opt-out from None = use cue-level). --idempotency-key (≤256 chars) Opaque dedup key (cueapi #683 Phase 2). Same key + same body within 24h returns the cached execution; same key + different body returns 409 idempotency_key_conflict. Implementation pin (caught earlier on cueapi-python #33 + cueapi-mcp #29): idempotency_key is a BODY field on cues fire, NOT the Idempotency-Key header. Server's FireRequest schema diverges from the messaging primitive's header-based idempotency. Help text + load-bearing test pin this so a future refactor doesn't 'simplify' to header-based (which would silently not work — server FireRequest is extra='forbid' for unknown body fields, ignores headers in body parsing). Tests added (3): - test_fire_help: pins all 5 flags appear in --help (--payload-override, --merge-strategy, --send-at, --exit-criteria, --idempotency-key) - test_fire_exit_criteria_repeatable_via_help: validates click parser accepts repeated --exit-criteria flags - test_fire_help_pins_idempotency_key_as_body_field: load-bearing pin against future header-refactor 185/185 tests pass (was 183; 2 new + parser-validation added inline). Source: drift audit handoff/cueapi-package-drift-2026-05-06; Backlog rows "Parity port: PR #632 → cueapi-cli" + "PR #683 not in backlog (folded in same-day)". Triggered by cueapi-main confirming bandwidth on cueapi-cli lane this session ([CC-CUEAPI-DOC-DEBT-CLOSED-DISCIPLINE- CODIFIED]). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 582815a commit b928837

2 files changed

Lines changed: 65 additions & 0 deletions

File tree

cueapi/cli.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,13 +464,38 @@ def delete(ctx: click.Context, cue_id: str, yes: bool) -> None:
464464
"'fire now' (idempotent — no error). Hosted PR #618."
465465
),
466466
)
467+
@click.option(
468+
"--exit-criteria",
469+
"exit_criteria",
470+
multiple=True,
471+
help=(
472+
"Required-assertion key for §14 work-verification-light (hosted PR #632). "
473+
"Repeat the flag for multiple keys (max 20). Receiver MUST report values for "
474+
"every key under outcome.assertions; missing keys mark the execution "
475+
"verification_failed. Pass --exit-criteria '' once to explicitly opt out of "
476+
"cue-level required_assertions for this fire."
477+
),
478+
)
479+
@click.option(
480+
"--idempotency-key",
481+
"idempotency_key",
482+
default=None,
483+
help=(
484+
"Opaque dedup key (≤256 chars, hosted PR #683). Same key on the same cue within "
485+
"24h returns the cached execution without firing again. Same key + DIFFERENT body "
486+
"returns 409 idempotency_key_conflict. Sent as a BODY field on cues fire (server "
487+
"FireRequest schema; diverges from messaging primitive's Idempotency-Key header)."
488+
),
489+
)
467490
@click.pass_context
468491
def fire(
469492
ctx: click.Context,
470493
cue_id: str,
471494
payload_override: Optional[str],
472495
merge_strategy: Optional[str],
473496
send_at: Optional[str],
497+
exit_criteria: tuple,
498+
idempotency_key: Optional[str],
474499
) -> None:
475500
"""Fire an existing cue immediately, optionally overriding its payload."""
476501
body: dict = {}
@@ -483,6 +508,16 @@ def fire(
483508
body["merge_strategy"] = merge_strategy
484509
if send_at:
485510
body["send_at"] = send_at
511+
if exit_criteria:
512+
# click multiple=True yields a tuple. Single '' entry opts out by sending [].
513+
if "" in exit_criteria:
514+
body["exit_criteria"] = []
515+
else:
516+
body["exit_criteria"] = list(exit_criteria)
517+
if idempotency_key:
518+
# Body field on cues fire (NOT header — server FireRequest schema diverges
519+
# from messaging primitive's Idempotency-Key header convention).
520+
body["idempotency_key"] = idempotency_key
486521

487522
try:
488523
with CueAPIClient(api_key=ctx.obj.get("api_key"), profile=ctx.obj.get("profile")) as client:

tests/test_cli.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,36 @@ def test_fire_help():
151151
assert "fire" in result.output.lower()
152152
assert "--payload-override" in result.output
153153
assert "--merge-strategy" in result.output
154+
# PR #618 + #632 + #683 parity
155+
assert "--send-at" in result.output
156+
assert "--exit-criteria" in result.output
157+
assert "--idempotency-key" in result.output
158+
159+
160+
def test_fire_exit_criteria_repeatable_via_help():
161+
"""--exit-criteria takes multiple values (click multiple=True). Verify the
162+
parser accepts repeated flags by passing them alongside --help."""
163+
result = runner.invoke(
164+
main,
165+
[
166+
"fire", "cue_x",
167+
"--exit-criteria", "task_completed",
168+
"--exit-criteria", "result_valid",
169+
"--help", # short-circuits actual fire — we just want parse to accept
170+
],
171+
)
172+
assert result.exit_code == 0
173+
174+
175+
def test_fire_help_pins_idempotency_key_as_body_field():
176+
"""Pin: the help text MUST call out that idempotency_key flows as a body
177+
field on cues fire (NOT a header), to prevent a future 'simplifying'
178+
refactor that moves it to a header — server's FireRequest schema is
179+
extra='forbid' and would NOT see it as a header. Same divergence I caught
180+
on cueapi-python #33 + cueapi-mcp #29."""
181+
result = runner.invoke(main, ["fire", "--help"])
182+
output = result.output.lower()
183+
assert "body field" in output or "body" in output
154184

155185

156186
def test_fire_requires_cue_id():

0 commit comments

Comments
 (0)