diff --git a/agentkit/.claude-plugin/plugin.json b/agentkit/.claude-plugin/plugin.json index d9fcc4e7..7a932e91 100644 --- a/agentkit/.claude-plugin/plugin.json +++ b/agentkit/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentkit", - "version": "0.9.14", + "version": "0.9.15", "description": "Board-aware parallel issue and PR review skills, with lifecycle hooks and a per-repository contract.", "author": { "name": "wrzonance", diff --git a/agentkit/.codex-plugin/plugin.json b/agentkit/.codex-plugin/plugin.json index 1c72cefe..944e5698 100644 --- a/agentkit/.codex-plugin/plugin.json +++ b/agentkit/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentkit", - "version": "0.9.14", + "version": "0.9.15", "description": "Board-aware parallel issue and PR review skills, with lifecycle hooks and a per-repository contract.", "author": { "name": "wrzonance", diff --git a/agentkit/skills/review-remote-pr/scripts/consent-record.sh b/agentkit/skills/review-remote-pr/scripts/consent-record.sh index fbf79226..735e4456 100755 --- a/agentkit/skills/review-remote-pr/scripts/consent-record.sh +++ b/agentkit/skills/review-remote-pr/scripts/consent-record.sh @@ -483,21 +483,117 @@ authorization_clause() { *) return 1 ;; esac } -has_authorized_relationship() { - local words=$1 provider=$2 model_words=$3 model_alias=$4 clause lead scope tail pattern model - clause=$(authorization_clause "$words") || return 1 +# Index (in characters) of the first standalone occurrence of `needle` inside +# `haystack`, or -1 when absent. Used to check relative ordering of the +# provider/model/purpose tokens within an authorization clause, independent +# of whatever filler words the operator wrote between them. +word_index() { + local haystack=" $1 " needle=$2 prefix + [[ $haystack == *" $needle "* ]] || { printf -- '-1'; return; } + prefix=${haystack%%" $needle "*} + printf '%s' "${#prefix}" +} +# General fallback for has_authorized_relationship: rather than requiring the +# exact grammar `[provider] [for] ` right after one of a +# fixed set of opening phrases, require only that a provider token, the +# model words, and a purpose phrase each appear somewhere in the WHOLE +# instruction, in that relative order -- any words before, between, or +# repeated verbs (a harness name, an effort level, "make sure you have ... +# have ... perform") are tolerated. The leading-word requirement rejects a +# bare label with nothing said about it (the provider/model span cannot be +# the very first word of the instruction). Bounded per #896 review: naming +# the harness/model in order is not itself an instruction, so the purpose +# must be governed by a performative verb, and any inquiry/explanation +# phrasing anywhere in the instruction refuses -- "explain how you would +# review X" names every element without asking for the review to happen. +# LOOSE_MATCH_REASON (global) carries the human-readable cause of a bounded +# refusal so affirmation_refusal can name it instead of a generic message. +has_ordered_authorization_words() { + local words=$1 provider=$2 model_words=$3 model_alias=$4 + local -a provider_tokens purpose_tokens=('adversarial review' 'cross review' review) + local -a inquiry_tokens=(explain describe 'how to' 'how do' 'what is' 'what would' \ + 'tell me' 'show me' document summarize summarise 'walk me through' \ + 'help me understand' teach) + local -a performative_tokens=(perform run 'do' execute conduct 'carry out' start \ + 'kick off' 'give me' 'get me') + # Deferral or retrospective wording BEFORE the performative verb ("when we are + # ready, have codex ... perform", "last time we had codex ... perform") is not a + # present instruction; the same words after the verb ("perform a review tomorrow") + # still are (CodeRabbit, PR #898). + local -a deferral_tokens=(when whenever after once until till before later tomorrow \ + tonight yesterday earlier previously wait eventually 'last time') + local token model idx provider_idx=-1 model_idx=-1 purpose_idx=-1 performative_idx=-1 first_idx + LOOSE_MATCH_REASON='' + for token in "${inquiry_tokens[@]}"; do + if has_words "$words" "$token"; then + LOOSE_MATCH_REASON='instruction asks for an explanation, not a review' + return 1 + fi + done case $provider in - anthropic) lead='((claude|anthropic)( with)? )?' ;; - openai) lead='((codex|openai)( with)? )?' ;; - *) lead="($(normalize_words "$provider")( with)? )?" ;; + anthropic) provider_tokens=(claude anthropic opus sonnet haiku) ;; + openai) provider_tokens=(codex openai gpt) ;; + *) provider_tokens=("$(normalize_words "$provider")") ;; esac - scope='(of this (pr|pull request|diff)|for this pr|on this pr|on it|of (the|that) diff|of (pr|pull request) [0-9]+)' - tail="(for )?(adversarial review|cross review|review)( $scope)?( do not ask again)?" + for token in "${provider_tokens[@]}"; do + [[ -n $token ]] || continue + idx=$(word_index "$words" "$token") + ((idx < 0)) && continue + { ((provider_idx < 0)) || ((idx < provider_idx)); } && provider_idx=$idx + done for model in "$model_words" "$model_alias"; do [[ -n $model && ! $model =~ ^[0-9]+$ ]] || continue - pattern="^${lead}${model} ${tail}$" - [[ $clause =~ $pattern ]] && return 0 - done; return 1 + idx=$(word_index "$words" "$model") + ((idx < 0)) && continue + { ((model_idx < 0)) || ((idx < model_idx)); } && model_idx=$idx + done + for token in "${purpose_tokens[@]}"; do + idx=$(word_index "$words" "$token") + ((idx < 0)) && continue + { ((purpose_idx < 0)) || ((idx < purpose_idx)); } && purpose_idx=$idx + done + ((model_idx >= 0 && purpose_idx >= 0 && model_idx < purpose_idx)) || return 1 + ((provider_idx < 0)) || ((provider_idx < model_idx)) || return 1 + first_idx=$model_idx + ((provider_idx < 0)) || first_idx=$provider_idx + ((first_idx > 0)) || return 1 + for token in "${performative_tokens[@]}"; do + idx=$(word_index "$words" "$token") + ((idx < 0)) && continue + { ((performative_idx < 0)) || ((idx < performative_idx)); } && performative_idx=$idx + done + if ((performative_idx < 0 || performative_idx >= purpose_idx)); then + LOOSE_MATCH_REASON='no performative verb governs the review purpose' + return 1 + fi + for token in "${deferral_tokens[@]}"; do + idx=$(word_index "$words" "$token") + if ((idx >= 0 && idx < performative_idx)); then + LOOSE_MATCH_REASON='instruction is conditional or not a present request' + return 1 + fi + done + return 0 +} +has_authorized_relationship() { + local words=$1 provider=$2 model_words=$3 model_alias=$4 clause lead scope tail pattern model + LOOSE_MATCH_REASON='' + if clause=$(authorization_clause "$words"); then + case $provider in + anthropic) lead='((claude|anthropic)( with)? )?' ;; + openai) lead='((codex|openai)( with)? )?' ;; + *) lead="($(normalize_words "$provider")( with)? )?" ;; + esac + scope='(of this (pr|pull request|diff)|for this pr|on this pr|on it|of (the|that) diff|of (pr|pull request) [0-9]+)' + tail="(for )?(adversarial review|cross review|review)( $scope)?( do not ask again)?" + for model in "$model_words" "$model_alias"; do + [[ -n $model && ! $model =~ ^[0-9]+$ ]] || continue + pattern="^${lead}${model} ${tail}$" + [[ $clause =~ $pattern ]] && return 0 + done + fi + has_ordered_authorization_words "$words" "$provider" "$model_words" "$model_alias" && return 0 + return 1 } strip_quoted_segments() { local input=$1 output='' quote='' char close='' previous='' following='' i curly_open=$'\u2018' curly_close=$'\u2019' @@ -525,10 +621,21 @@ strip_quoted_segments() { } affirmation_refusal() { local provider_found=$1 model_found=$2 purpose_found=$3 provider_spellings=$4 model_spellings=$5 + local unparsed_clause=${6:-0} reason=${7:-} not_affirmative=${8:-0} record_refused_grant || die 'cannot persist refused-grant provenance' ((provider_found)) || printf '%s: operator instruction missing provider; accepted: %s\n' "$PROGNAME" "$provider_spellings" >&2 ((model_found)) || printf '%s: operator instruction missing model; accepted: %s\n' "$PROGNAME" "$model_spellings" >&2 - ((purpose_found)) || printf '%s: operator instruction missing purpose; accepted: adversarial review, review, cross-review\n' "$PROGNAME" >&2 + if ((! purpose_found)); then + if ((provider_found && model_found && not_affirmative)); then + printf '%s: instruction is not affirmative\n' "$PROGNAME" >&2 + elif ((provider_found && model_found && unparsed_clause)) && [[ -n $reason ]]; then + printf '%s: %s\n' "$PROGNAME" "$reason" >&2 + elif ((provider_found && model_found && unparsed_clause)); then + printf '%s: could not parse an authorization clause; found provider, model and purpose\n' "$PROGNAME" >&2 + else + printf '%s: operator instruction missing purpose; accepted: adversarial review, review, cross-review\n' "$PROGNAME" >&2 + fi + fi exit 2 } @@ -536,6 +643,7 @@ validate_operator_affirmation() { local instruction=${OPERATOR_INSTRUCTION,,} destination=${DESTINATION,,} outside words full_words destination_words purpose_words local model_words model_alias model_spellings provider_spellings provider_found=0 model_found=0 purpose_found=0 local affirmative=0 safe=1 payload_pr explicit_pr token purpose_pattern='^(one )?(adversarial review|cross review|review)( of (that|this) (diff|pr|pull request))?$' + local purpose_found_raw field_is_safe "$MODEL" || die_usage 'model must be non-empty and delimiter-free' outside=$(strip_quoted_segments "$instruction") || outside='' words=$(normalize_words "$outside") @@ -579,6 +687,7 @@ validate_operator_affirmation() { purpose_found=1 fi [[ $purpose_words =~ $purpose_pattern ]] || purpose_found=0 + purpose_found_raw=$purpose_found has_authorized_relationship "$words" "$PROVIDER" "$model_words" "$model_alias" && affirmative=1 full_words=${full_words%' do not ask again'} for token in no not never dont 'don t' cannot cant 'can t' refuse refused declines declined decline avoid without except forbid forbidden revoke revoked instead if unless rather; do @@ -592,9 +701,13 @@ validate_operator_affirmation() { explicit_pr=${BASH_REMATCH[3]} [[ $explicit_pr == "$payload_pr" ]] || safe=0 fi + local unparsed_clause=0 not_affirmative=0 + ((provider_found && model_found && purpose_found_raw && safe && ! affirmative)) && unparsed_clause=1 + ((provider_found && model_found && purpose_found_raw && ! safe)) && not_affirmative=1 ((affirmative && safe)) || purpose_found=0 ((provider_found && model_found && purpose_found)) || - affirmation_refusal "$provider_found" "$model_found" "$purpose_found" "$provider_spellings" "$model_spellings" + affirmation_refusal "$provider_found" "$model_found" "$purpose_found" "$provider_spellings" "$model_spellings" \ + "$unparsed_clause" "$LOOSE_MATCH_REASON" "$not_affirmative" } grant_command() { [[ $SOURCE == interactive || $SOURCE == auto-review-flag || $SOURCE == operator-instruction ]] || diff --git a/opencode/package.json b/opencode/package.json index 780b1677..582a2e33 100644 --- a/opencode/package.json +++ b/opencode/package.json @@ -1,6 +1,6 @@ { "name": "@wrzonance/agentkit-opencode", - "version": "0.9.14", + "version": "0.9.15", "description": "Agent Kit plugin for OpenCode CLI: injects the environment contract into the model's system prompt at session start.", "type": "module", "main": "./index.js", diff --git a/plugin/agentkit/.claude-plugin/plugin.json b/plugin/agentkit/.claude-plugin/plugin.json index d9fcc4e7..7a932e91 100644 --- a/plugin/agentkit/.claude-plugin/plugin.json +++ b/plugin/agentkit/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentkit", - "version": "0.9.14", + "version": "0.9.15", "description": "Board-aware parallel issue and PR review skills, with lifecycle hooks and a per-repository contract.", "author": { "name": "wrzonance", diff --git a/plugin/agentkit/.codex-plugin/plugin.json b/plugin/agentkit/.codex-plugin/plugin.json index 1c72cefe..944e5698 100644 --- a/plugin/agentkit/.codex-plugin/plugin.json +++ b/plugin/agentkit/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "agentkit", - "version": "0.9.14", + "version": "0.9.15", "description": "Board-aware parallel issue and PR review skills, with lifecycle hooks and a per-repository contract.", "author": { "name": "wrzonance", diff --git a/plugin/opencode/package.json b/plugin/opencode/package.json index 780b1677..582a2e33 100644 --- a/plugin/opencode/package.json +++ b/plugin/opencode/package.json @@ -1,6 +1,6 @@ { "name": "@wrzonance/agentkit-opencode", - "version": "0.9.14", + "version": "0.9.15", "description": "Agent Kit plugin for OpenCode CLI: injects the environment contract into the model's system prompt at session start.", "type": "module", "main": "./index.js", diff --git a/tests/lint-helper-size.sh b/tests/lint-helper-size.sh index 344aa7e5..bfab1b1c 100755 --- a/tests/lint-helper-size.sh +++ b/tests/lint-helper-size.sh @@ -60,6 +60,18 @@ declare -A KNOWN_OVERSIZE=( [skills/review-remote-pr/scripts/adversarial-run.sh]="1016:12762:800" # #728 separates optional CI outcomes from required acceptance execution. [skills/review-remote-pr/scripts/gh-pr-state.sh]="1208:14622:800" + # #896: tolerate ordinary filler words between provider/model/purpose in + # an operator-instruction authorization clause, and name an unparsed + # clause instead of misreporting it as a missing purpose. + # #896 fix round: the ordered check now runs over the whole instruction, + # not just a fixed-opener clause, so a doubled verb with no recognized + # opener at all still grants. + # #896 P1 review repair: bound the fallback to a performative verb + # governing the purpose and refuse any inquiry/explanation phrasing. + # #896 round 4: distinct "not affirmative" message for a negated + # instruction that still satisfies the ordered/performative checks. + # PR #898 CodeRabbit: refuse deferral/retrospective wording before the performative verb. + [skills/review-remote-pr/scripts/consent-record.sh]="911:10831:800" # #706 skip-provenance refusal plus #707 observed CI evidence. # #717: validated attempt provenance in receipts and remote ledger entries. [skills/review-remote-pr/scripts/post-receipt.sh]="994:11350:800" @@ -205,7 +217,11 @@ readonly MAX_HELPER_TOKENS=10000 # activation-gate option B task 2: --activation-nonce flag folds the receipt # into agent-preflight.sh's first call; exact combined helper tree measurement. # final-review Minor #2: +67 tokens from agent-preflight.sh's usage-error guard. -readonly MAX_TREE_TOKENS=477662 +# #896: consent-record.sh crosses into KNOWN_OVERSIZE; exact tree measurement. +# #896 fix round: whole-instruction ordered check; exact tree measurement. +# #896 P1 review repair: performative-verb bound; exact tree measurement. +# #896 round 4: not-affirmative refusal message; exact tree measurement. +readonly MAX_TREE_TOKENS=479152 violations=0 checked=0 diff --git a/tests/test-consent-record-affirmation.sh b/tests/test-consent-record-affirmation.sh index 8139ef01..16dabc3e 100755 --- a/tests/test-consent-record-affirmation.sh +++ b/tests/test-consent-record-affirmation.sh @@ -166,6 +166,155 @@ assert_contains "$missing_purpose_out" 'missing purpose' 'purpose refusal names assert_contains "$missing_purpose_out" 'adversarial review, review, cross-review' \ 'purpose refusal gives accepted spellings' +# #896: an imperative turn with ordinary filler words between provider, model +# and purpose grants -- it must not be misdiagnosed as a missing purpose. +issue896_state="$tmp/state/issue-896" +issue896_out=$(bash "$consent" grant --state "$issue896_state" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'Have the local codex harness with gpt-6-astra at xhigh effort perform an adversarial review' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" 2>&1) +issue896_rc=$? +assert_eq 0 "$issue896_rc" 'the #896 verbatim operator turn grants despite filler words' +assert_contains "$issue896_out" 'source=operator-instruction' 'the #896 grant retains operator provenance' + +# The same sentence negated still refuses. +issue896_negated_state="$tmp/state/issue-896-negated" +issue896_negated_rc=0 +bash "$consent" grant --state "$issue896_negated_state" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'Have the local codex harness with gpt-6-astra at xhigh effort do not perform an adversarial review' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" >/dev/null 2>&1 || issue896_negated_rc=$? +assert_eq 2 "$issue896_negated_rc" 'a negated form of the #896 turn is still refused' +assert_eq no \ + "$([[ -e $issue896_negated_state || -e $issue896_negated_state.decision.json || -e $issue896_negated_state.consent-paths ]] && printf yes || printf no)" \ + 'the negated #896 turn creates no grant evidence' + +# #896 fix round: a second real operator turn, with a doubled "have" and no +# clause opener the grammar recognizes at all, must also grant -- the ordered +# check has to work over the whole instruction, not just a stripped clause. +issue896b_state="$tmp/state/issue-896b" +issue896b_out=$(bash "$consent" grant --state "$issue896b_state" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'make sure you have codex have gpt-6-astra xhigh perform an adversarial review' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" 2>&1) +issue896b_rc=$? +assert_eq 0 "$issue896b_rc" 'a real turn with a doubled "have" and no recognized opener still grants' +assert_contains "$issue896b_out" 'source=operator-instruction' 'the #896 fix-round grant retains operator provenance' + +# The same sentence negated still refuses. +issue896b_negated_state="$tmp/state/issue-896b-negated" +issue896b_negated_rc=0 +bash "$consent" grant --state "$issue896b_negated_state" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'make sure you do not have codex have gpt-6-astra xhigh perform an adversarial review' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" >/dev/null 2>&1 || issue896b_negated_rc=$? +assert_eq 2 "$issue896b_negated_rc" 'a negated form of the second #896 turn is still refused' +assert_eq no \ + "$([[ -e $issue896b_negated_state || -e $issue896b_negated_state.decision.json || -e $issue896b_negated_state.consent-paths ]] && printf yes || printf no)" \ + 'the negated second #896 turn creates no grant evidence' + +# When provider, model and purpose are all present but out of order, the +# refusal names the real cause instead of misreporting a missing purpose. +unparseable_out=$(bash "$consent" grant --state "$tmp/state/issue-896-unparseable" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'Perform an adversarial review using gpt-6-astra hosted via codex' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" 2>&1) +assert_contains "$unparseable_out" 'could not parse an authorization clause; found provider, model and purpose' \ + 'an out-of-order but complete instruction names the real refusal cause' + +# #896 P1 (adversarial review): naming provider/model/purpose in order is not +# itself an instruction -- an explanation request must still refuse even +# though every element is present in order. +explain_state="$tmp/state/issue-896-explain" +explain_rc=0 +explain_out=$(bash "$consent" grant --state "$explain_state" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'Use codex with gpt-6-astra to explain how to request consent for an adversarial review' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" 2>&1) || explain_rc=$? +assert_eq 2 "$explain_rc" 'an explanation request is refused even with provider, model and purpose in order' +assert_contains "$explain_out" 'instruction asks for an explanation, not a review' \ + 'the explanation refusal names its real cause' +assert_eq no \ + "$([[ -e $explain_state || -e $explain_state.decision.json || -e $explain_state.consent-paths ]] && printf yes || printf no)" \ + 'the explanation refusal creates no grant evidence' + +# A second explanation phrasing ("tell me how you would do X") also refuses +# with the same message, even though it also contains the performative verb +# "do" -- the inquiry check wins. +tellme_rc=0 +tellme_out=$(bash "$consent" grant --state "$tmp/state/issue-896-tellme" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'codex with gpt-6-astra, tell me how you would do an adversarial review' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" 2>&1) || tellme_rc=$? +assert_eq 2 "$tellme_rc" '"tell me how you would do X" is still an explanation request, not an instruction' +assert_contains "$tellme_out" 'instruction asks for an explanation, not a review' \ + 'the "tell me how you would do X" refusal names the same explanation cause' + +# A negated form of the doubled-"have" turn refuses with an affirmative- +# specific message, not a misleading "missing purpose" -- ordering and the +# performative verb ("perform") are still present, only the negation blocks it. +not_affirmative_rc=0 +not_affirmative_out=$(bash "$consent" grant --state "$tmp/state/issue-896-not-affirmative" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'make sure you have codex have gpt-6-astra xhigh do not perform an adversarial review' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" 2>&1) || not_affirmative_rc=$? +assert_eq 2 "$not_affirmative_rc" 'a negated doubled-"have" turn refuses' +assert_contains "$not_affirmative_out" 'instruction is not affirmative' \ + 'the negated refusal names its real cause instead of a missing purpose' +assert_eq no \ + "$([[ -e $tmp/state/issue-896-not-affirmative || -e $tmp/state/issue-896-not-affirmative.decision.json \ + || -e $tmp/state/issue-896-not-affirmative.consent-paths ]] && printf yes || printf no)" \ + 'the negated refusal creates no grant evidence' + +# CodeRabbit (PR #898): deferral or retrospective wording BEFORE the performative verb +# is not a present instruction; a time reference AFTER the verb still is. +deferred_rc=0 +deferred_out=$(bash "$consent" grant --state "$tmp/state/issue-896-deferred" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'when we are ready, have codex with gpt-6-astra perform an adversarial review' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" 2>&1) || deferred_rc=$? +assert_eq 2 "$deferred_rc" 'a deferred "when we are ready" turn refuses' +assert_contains "$deferred_out" 'instruction is conditional or not a present request' \ + 'the deferred refusal names its cause' +retro_rc=0 +retro_out=$(bash "$consent" grant --state "$tmp/state/issue-896-retro" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'last time we had codex with gpt-6-astra perform an adversarial review' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" 2>&1) || retro_rc=$? +assert_eq 2 "$retro_rc" 'a retrospective "last time" turn refuses' +assert_contains "$retro_out" 'instruction is conditional or not a present request' \ + 'the retrospective refusal names its cause' +assert_rc 0 'a present request with a time reference after the verb still grants' -- \ + bash "$consent" grant --state "$tmp/state/issue-896-tomorrow" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'have codex with gpt-6-astra perform an adversarial review tomorrow' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" + +# The two prior #896 grants still hold with the performative-verb bound in place. +assert_rc 0 'the doubled-"have" #896 turn still grants under the performative-verb bound' -- \ + bash "$consent" grant --state "$tmp/state/issue-896b-reverify" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'make sure you have codex have gpt-6-astra xhigh perform an adversarial review' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" +assert_rc 0 'the original #896 turn still grants under the performative-verb bound' -- \ + bash "$consent" grant --state "$tmp/state/issue-896-reverify" --provider codex \ + --payload "$payload" --source operator-instruction \ + --operator-instruction 'Have the local codex harness with gpt-6-astra at xhigh effort perform an adversarial review' \ + --destination 'OpenAI via the local codex CLI (gpt-6-astra)' --model gpt-6-astra \ + --purpose 'adversarial review' --paths-file "$tmp/paths" + # Provider aliases and natural model spellings are token-bounded and provider-specific. for accepted in \ 'Each PR is authorized to have one anthropic Opus 5 xhigh review.' \