Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions .agents/skills/launch-openshell-gator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ For gator's PR/issue validation policy, load `gator-gate` inside the launched sa
| `scripts/agents/gator/policy.yaml` | Sandbox policy for the gator agent. |
| `scripts/agents/gator/bin/gh` | Gator-specific `gh` wrapper and same-SHA duplicate-post guard. |
| `scripts/agents/gator/bin/review-feedback-ledger` | Builds tree-aware review scope, durable findings, convergence telemetry, and review-budget state. |
| `scripts/agents/gator/bin/resolve-gator-review-threads` | Resolves addressed Gator-owned inline review threads by ledger finding ID. |
| `scripts/agents/gator/bin/validate-review-findings` | Enforces the blocker evidence schema and downgrades unsupported hypotheses. |
| `scripts/agents/gator/prompts/gator.md` | Rendered top-level prompt template baked into the payload. |
| `scripts/agents/gator/skills/gator-gate/SKILL.md` | In-sandbox gator state-machine skill. |
Expand Down Expand Up @@ -157,7 +158,7 @@ sandbox_name="gator-pr-${pr_number}-supervised"
"Review and monitor PR #${pr_number} through the gator-gate workflow. Scope this invocation only to PR #${pr_number}."
```

The launcher builds the gator sandbox image when needed, stages the immutable payload, imports provider profiles, configures provider credentials and refresh, creates the sandbox, and writes a background log under `scripts/agents/gator/logs/`.
The launcher builds the gator sandbox image when needed, stages the immutable payload, imports provider profiles, configures provider credentials and refresh, creates and uploads the sandbox payload, then starts the agent supervisor with `sandbox exec`. It writes a background log under `scripts/agents/gator/logs/`.

### Launch An Issue Or Issue/PR Pair

Expand Down Expand Up @@ -317,10 +318,10 @@ Restart when the payload must change, the sandbox is wedged without a sentinel,

Increment `payload_version` in `scripts/agents/gator/agent.yaml` whenever a
merged change alters the Gator prompt, gate skill, reviewer contract, write
guard, ledger, or bundled validator. Existing immutable watchers cannot replace
their own payload. New-version watchers detect later published versions and
stop with `stale_gator_payload`; relaunch every still-active older watcher after
the version bump is published.
guard, ledger, thread resolver, or bundled validator. Existing immutable
watchers cannot replace their own payload. New-version watchers detect later
published versions and stop with `stale_gator_payload`; relaunch every
still-active older watcher after the version bump is published.

Before deleting, check that the sandbox is truly stale or that the operator asked for a restart. If a bounded review cycle is actively running and still producing useful output, prefer leaving it alone.

Expand Down Expand Up @@ -368,7 +369,9 @@ Symptoms: host `gh` auth fails, Codex refresh fails, in-sandbox GitHub calls rep
Actions:

- Re-run the GitHub and Codex preflight checks.
- Existing refresh-managed providers are reused without an ordinary credential update; the launcher rotates their gateway-managed credential instead.
- If host Codex auth changed, relaunch with `--reset-refresh` once.
- `--reset-refresh` removes the old refresh ownership before rediscovering host credentials, then configures and rotates the replacement refresh state.
- If Entra or Microsoft auth is involved in a future provider, use the relevant auth skill. Gator's default providers are GitHub and Codex.

### Unsupported `gh pr view --json` Field
Expand Down
2 changes: 2 additions & 0 deletions scripts/agents/gator/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,12 @@ RUN mkdir -p /etc/openshell
COPY policy.yaml /etc/openshell/policy.yaml
COPY bin/gh /usr/local/bin/gh-gator
COPY bin/review-feedback-ledger /usr/local/bin/review-feedback-ledger
COPY bin/resolve-gator-review-threads /usr/local/bin/resolve-gator-review-threads
COPY bin/validate-review-findings /usr/local/bin/validate-review-findings
RUN rm -f /usr/local/bin/gh && \
cp /usr/local/bin/gh-gator /usr/local/bin/gh && \
chmod 755 /usr/local/bin/gh /usr/local/bin/review-feedback-ledger \
/usr/local/bin/resolve-gator-review-threads \
/usr/local/bin/validate-review-findings

RUN printf 'export PATH="/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin"\nexport PS1="\\u@\\h:\\w\\$ "\n' \
Expand Down
10 changes: 8 additions & 2 deletions scripts/agents/gator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The manifest-driven launcher at `scripts/agents/run.sh` reads `agent.yaml`, whic

The launcher:

- Scans `profile_paths` in manifest order and imports `providers/github-gator.yaml`.
- Scans `profile_paths` in manifest order and imports or updates `providers/github-gator.yaml`.
- Creates or updates the `github-gator` provider from `gh auth token`.
- Selects the requested harness and bakes the common runtime into the immutable sandbox payload.
- For `--harness codex`, imports `providers/codex-gator.yaml`, creates or updates the `codex-gator` provider from `$HOME/.codex/auth.json`, and stores the refresh token as gateway-only refresh material.
Expand All @@ -38,14 +38,18 @@ The launcher:
- Uses the gator image policy copied to `/etc/openshell/policy.yaml`.
- Installs the gator-specific `gh` wrapper from `gator/bin/gh` as `/usr/local/bin/gh` to fail closed when same-head-SHA history cannot be checked, prevent duplicate dispositions, and require versioned review payloads.
- Installs `gator/bin/review-feedback-ledger` as `/usr/local/bin/review-feedback-ledger` so reviews receive tree- and patch-aware scope, prior summaries and findings, resolution state, convergence telemetry, and the three-round Warning budget.
- Installs `gator/bin/resolve-gator-review-threads` so a follow-up commit that
demonstrably fixes a Gator inline finding can resolve the corresponding
Gator-owned GitHub review thread without touching human review threads.
- Installs `gator/bin/validate-review-findings` to downgrade blockers that lack the required reachability, ownership, base-vs-head, impact, and reproducer evidence.
- Keeps that normalized evidence as Gator's internal review contract, then renders validated blockers for people as a read-aloud `Summary`, an actionable `Fix`, and a deterministic `Verify`. Exact paths and only the additional provenance an implementation agent needs appear in collapsed `Agent context`; raw evidence headings such as `Base` and `Head` are not posted publicly. Review-process provenance, docs and E2E disposition, SHAs, and state codes appear at the end of the summary in collapsed `Gator metadata`, while required human actions remain visible.
- Bakes `scripts/agents/gator/skills/gator-gate/SKILL.md` into `/etc/openshell/agent-payload`.
- Bakes `.claude/agents/principal-engineer-reviewer.md` so the selected harness can run a deterministic independent reviewer execution through `/etc/openshell/agent-payload/runtime/subagent.sh principal-engineer-reviewer < task.md`.
- For `--harness codex`, optionally bakes a host Codex executable as `/etc/openshell/agent-payload/runtime/harnesses/codex/codex`.
- Starts the selected harness without a TTY.
- Runs gator in `watch` mode by default. The sandbox stays alive while the supervisor sleeps between bounded Codex cycles, so Codex is not connected during passive PR waits. The supervisor prints periodic heartbeat lines during active cycles and passive sleeps.
- Makes each watch cycle compare its immutable payload version with the version published on the default branch. A stale watcher stops without GitHub writes and must be relaunched.
The GitHub provider profile allows read-only GraphQL queries on `api.github.com/graphql` so `gh` read paths can use GraphQL when needed. Write operations remain REST-only and scoped to the two allowed repositories.
The GitHub provider profile allows read-only GraphQL queries on `api.github.com/graphql` so `gh` read paths can use GraphQL when needed. Its only GraphQL mutation is the named `ResolveGatorReviewThread` operation, restricted to the `resolveReviewThread` root field. All other writes remain REST-only and scoped to the two allowed repositories.

Set `GATOR_CODEX_ACCESS_CREDENTIAL_KEY` or pass `--codex-access-key` if the gator Codex profile uses a credential key other than `CODEX_AUTH_ACCESS_TOKEN` for the short-lived access token.

Expand All @@ -58,4 +62,6 @@ The launcher preserves existing gateway-owned Codex refresh material by default
```shell
bash scripts/agents/gator/bin/gh_guard_test.sh
bash scripts/agents/gator/bin/review_feedback_ledger_test.sh
bash scripts/agents/gator/bin/resolve_gator_review_threads_test.sh
bash scripts/agents/runtime/harnesses/codex/exec_test.sh
```
2 changes: 1 addition & 1 deletion scripts/agents/gator/agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0

id: gator
payload_version: 4
payload_version: 7
display_name: Gator Gate Agent
description: Validate and monitor OpenShell GitHub issues and pull requests through the gator state machine.

Expand Down
2 changes: 1 addition & 1 deletion scripts/agents/gator/bin/gh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ set -euo pipefail

REAL_GH="${OPENSHELL_REAL_GH:-/usr/bin/gh}"
GATOR_MARKER='> **gator-agent**'
GATOR_PAYLOAD_VERSION="${OPENSHELL_AGENT_PAYLOAD_VERSION:-4}"
GATOR_PAYLOAD_VERSION="${OPENSHELL_AGENT_PAYLOAD_VERSION:-5}"

if [[ $# -lt 1 || "$1" != "api" ]]; then
exec "$REAL_GH" "$@"
Expand Down
19 changes: 13 additions & 6 deletions scripts/agents/gator/bin/gh_guard_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,15 @@ run_review_case() {

## PR Review Status

Head SHA: `0e4d7af7722fbedce2307d571b0c937a1eb3250f`' \
--arg payload 'Gator payload: `4`' \
The review is ready for author follow-up.

<details>
<summary>Gator metadata</summary>

- Head SHA: `0e4d7af7722fbedce2307d571b0c937a1eb3250f`' \
--arg payload '- Gator payload: `5`

</details>' \
--arg inline_body '> **gator-agent**

**Warning:** Keep this validation bound to the accepted value.' \
Expand Down Expand Up @@ -142,7 +149,7 @@ same_sha_body='> **gator-agent**
## PR Review Status

Head SHA: `0e4d7af7722fbedce2307d571b0c937a1eb3250f`
Gator payload: `4`'
Gator payload: `5`'

run_case "blocks duplicate marked comment" \
"$same_sha_body" \
Expand All @@ -169,7 +176,7 @@ run_case "allows first versioned review disposition" \
## PR Review Status

Head SHA: `0e4d7af7722fbedce2307d571b0c937a1eb3250f`
Gator payload: `4`' \
Gator payload: `5`' \
0

run_case "allows unmarked comment" \
Expand Down Expand Up @@ -224,7 +231,7 @@ Gator is blocked from completing the required independent re-review for current
## PR Review Status

Head SHA: `0e4d7af7722fbedce2307d571b0c937a1eb3250f`
Gator payload: `4`' \
Gator payload: `5`' \
0

draft_blocked_body='> **gator-agent**
Expand All @@ -244,7 +251,7 @@ run_case "ignores draft blocker after PR is ready" \
## PR Review Status

Head SHA: `0e4d7af7722fbedce2307d571b0c937a1eb3250f`
Gator payload: `4`' \
Gator payload: `5`' \
0 \
false

Expand Down
106 changes: 106 additions & 0 deletions scripts/agents/gator/bin/resolve-gator-review-threads
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env bash

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

set -euo pipefail

usage() {
cat >&2 <<'EOF'
Usage:
resolve-gator-review-threads LEDGER.json FINDING_ID [FINDING_ID ...]
EOF
}

[[ "$#" -ge 2 && -r "$1" ]] || {
usage
exit 2
}

ledger="$1"
shift

jq -e '
.schema_version == 4 and
(.threads | type == "array")
' "$ledger" >/dev/null || {
echo "invalid gator review feedback ledger" >&2
exit 2
}

mutation='mutation ResolveGatorReviewThread($threadId: ID!) {
resolveReviewThread(input: {threadId: $threadId}) {
thread {
id
isResolved
}
}
}'

finding_ids=("$@")
thread_ids=()
resolution_states=()
declare -A seen_finding_ids=()

# Validate the complete request before performing the first GitHub write so a
# malformed or ambiguous later ID cannot leave a partially resolved batch.
for finding_id in "${finding_ids[@]}"; do
[[ "$finding_id" =~ ^(GATOR-[0-9A-Fa-f]{8}-[0-9]{2}|gator-inline-[0-9]+)$ ]] || {
echo "invalid gator finding ID: $finding_id" >&2
exit 2
}
[[ -z "${seen_finding_ids[$finding_id]:-}" ]] || {
echo "duplicate gator finding ID: $finding_id" >&2
exit 2
}
seen_finding_ids["$finding_id"]=1

matching_threads="$(jq -c --arg finding_id "$finding_id" '
[
.threads[]
| select(.finding_id == $finding_id)
]
' "$ledger")"
match_count="$(jq 'length' <<< "$matching_threads")"
[[ "$match_count" -eq 1 ]] || {
echo "expected exactly one Gator thread for $finding_id; found $match_count" >&2
exit 1
}

first_body="$(jq -r '.[0].comments[0].body // empty' <<< "$matching_threads")"
[[ "$first_body" == '> **gator-agent**'* ]] || {
echo "refusing to resolve non-Gator thread for $finding_id" >&2
exit 1
}

thread_id="$(jq -r '.[0].thread_id // empty' <<< "$matching_threads")"
[[ -n "$thread_id" ]] || {
echo "Gator thread ID missing for $finding_id" >&2
exit 1
}

thread_ids+=("$thread_id")
resolution_states+=("$(jq -r '.[0].is_resolved' <<< "$matching_threads")")
done

for ((i = 0; i < ${#finding_ids[@]}; i++)); do
finding_id="${finding_ids[$i]}"
thread_id="${thread_ids[$i]}"
if [[ "${resolution_states[$i]}" == "true" ]]; then
echo "Gator review thread already resolved: $finding_id"
continue
fi

response="$(gh api graphql \
-f query="$mutation" \
-f threadId="$thread_id")"
jq -e --arg thread_id "$thread_id" '
.data.resolveReviewThread.thread
| .id == $thread_id and .isResolved == true
' <<< "$response" >/dev/null || {
echo "GitHub did not confirm resolution for $finding_id ($thread_id)" >&2
exit 1
}

echo "Resolved Gator review thread: $finding_id"
done
112 changes: 112 additions & 0 deletions scripts/agents/gator/bin/resolve_gator_review_threads_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env bash

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GATOR_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
RESOLVER="$SCRIPT_DIR/resolve-gator-review-threads"

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/bin"

cat > "$tmp/bin/gh" <<'SH'
#!/usr/bin/env bash
set -euo pipefail

thread_id=""
previous=""
for arg in "$@"; do
if [[ "$previous" == "-f" && "$arg" == threadId=* ]]; then
thread_id="${arg#threadId=}"
fi
previous="$arg"
done

printf '%s\n' "$*" >> "${MOCK_GH_LOG:?}"
if [[ "$thread_id" == "${MOCK_FAIL_THREAD:-}" ]]; then
jq -n --arg id "$thread_id" '{data: {resolveReviewThread: {thread: {id: $id, isResolved: false}}}}'
else
jq -n --arg id "$thread_id" '{data: {resolveReviewThread: {thread: {id: $id, isResolved: true}}}}'
fi
SH
chmod +x "$tmp/bin/gh"

cat > "$tmp/ledger.json" <<'JSON'
{
"schema_version": 4,
"threads": [
{
"thread_id": "thread-open",
"finding_id": "GATOR-11111111-01",
"is_resolved": false,
"comments": [{"body": "> **gator-agent**\n\nOpen finding"}]
},
{
"thread_id": "thread-resolved",
"finding_id": "GATOR-11111111-02",
"is_resolved": true,
"comments": [{"body": "> **gator-agent**\n\nResolved finding"}]
},
{
"thread_id": "thread-legacy",
"finding_id": "gator-inline-1234",
"is_resolved": false,
"comments": [{"body": "> **gator-agent**\n\nLegacy finding"}]
},
{
"thread_id": "thread-human",
"finding_id": "GATOR-22222222-01",
"is_resolved": false,
"comments": [{"body": "Human review thread"}]
}
]
}
JSON

export MOCK_GH_LOG="$tmp/gh.log"
PATH="$tmp/bin:$PATH" "$RESOLVER" "$tmp/ledger.json" \
GATOR-11111111-01 GATOR-11111111-02 gator-inline-1234 \
> "$tmp/success.out"

[[ "$(grep -c 'threadId=thread-' "$tmp/gh.log")" -eq 2 ]]
grep -q 'ResolveGatorReviewThread' "$tmp/gh.log"
grep -q 'threadId=thread-open' "$tmp/gh.log"
grep -q 'threadId=thread-legacy' "$tmp/gh.log"
grep -q 'already resolved: GATOR-11111111-02' "$tmp/success.out"

if PATH="$tmp/bin:$PATH" "$RESOLVER" "$tmp/ledger.json" GATOR-33333333-01 >/dev/null 2>&1; then
echo "expected unknown finding ID to fail" >&2
exit 1
fi

if PATH="$tmp/bin:$PATH" "$RESOLVER" "$tmp/ledger.json" GATOR-22222222-01 >/dev/null 2>&1; then
echo "expected human-owned thread resolution to fail" >&2
exit 1
fi

if MOCK_FAIL_THREAD=thread-open PATH="$tmp/bin:$PATH" \
"$RESOLVER" "$tmp/ledger.json" GATOR-11111111-01 >/dev/null 2>&1; then
echo "expected unconfirmed GitHub resolution to fail" >&2
exit 1
fi

ruby -ryaml -e '
profile = YAML.load_file(ARGV.fetch(0))
endpoint = profile.fetch("endpoints").find {
|entry| entry["path"] == "/graphql"
}
abort unless endpoint.fetch("rules").any? {
|rule|
allow = rule["allow"]
allow &&
allow["operation_type"] == "mutation" &&
allow["operation_name"] == "ResolveGatorReviewThread" &&
allow["fields"] == ["resolveReviewThread"]
}
' "$GATOR_DIR/providers/github-gator.yaml"

echo "resolve-gator-review-threads tests passed"
Loading
Loading