Skip to content

Get a reply to every request from an API that answers in HTTP/1.0, as Python's http.server does - #436

Merged
ssegura merged 1 commit into
masterfrom
fix/http-1-0-closed-connections
Nov 1, 2026
Merged

ssegura merged 1 commit into
masterfrom
fix/http-1-0-closed-connections

Conversation

@ssegura

@ssegura ssegura commented Nov 1, 2026

Copy link
Copy Markdown
Collaborator

Related: None

What you can do now that you could not before

You can test an API that answers in HTTP/1.0 and closes the connection after each reply, and get a
reply to every request. Python's own http.server works this way unless told otherwise, and many
small APIs under development run on it. Until now every other request went out on a connection the
API had already closed and was counted under no reply: half of the run's budget was lost, and the
report blamed the API.

Try it yourself

This needs python3 as well as Java and git, for the API: Python's own file server, which answers in
HTTP/1.0.

git fetch && git switch fix/http-1-0-closed-connections
./mvnw -q package -DskipTests
mkdir -p /tmp/http10 && echo hello > /tmp/http10/hello.txt
python3 -m http.server 8010 --bind 127.0.0.1 --directory /tmp/http10 2>/dev/null &
printf 'openapi: 3.0.3\ninfo: {title: Files, version: "1"}\npaths:\n  /hello.txt:\n    get:\n      responses:\n        "200":\n          description: the file\n          content:\n            text/plain:\n              schema: {type: string}\n' > /tmp/http10.yaml
./restest run /tmp/http10.yaml --url http://127.0.0.1:8010 --budget 10s --set engine.maxConcurrency=4
kill %1

Expected, on this branch (Python 3.9.6, macOS):
  50994 requests to 1 operations in 10.0s, 3% of it idle
    opening lap: 1 request in 65ms, 1 of 1 operations answered 2xx
    50994 2xx                                      <- every request answered

On master, the same commands print:
  91310 requests to 1 operations in 10.0s, 5% of it idle
    opening lap: 1 request in 64ms, 1 of 1 operations answered 2xx
    45655 2xx, 45655 no reply                      <- every other one lost

engine.maxConcurrency=4 keeps the run within what Python's server takes at once: it queues at most
five connections waiting to be accepted and refuses the rest (see "Open questions").

The engine's own test shows it without Python:

./mvnw -q verify -pl restest-exec -am -Dtest=ConnectionsTheApiClosesTest -Dsurefire.failIfNoSpecifiedTests=false

How it works

A connection is the line between RESTest and the API. RESTest keeps the line open after a reply, so
that the next request need not open a new one. A server that answers in HTTP/1.0 hangs up after every
reply unless the reply asks to keep the line, but the HTTP library RESTest uses only listens for the
HTTP/1.1 way of saying it will hang up (Connection: close). So it kept the dead line and sent the
next request down it: nobody heard it, and it came back with no reply. The request after that opened
a new line and was answered: exactly half of all requests were lost, every other one.

Now, when a reply arrives in HTTP/1.0 without asking to keep the line, RESTest marks that line as one
it will send nothing more on. Before reusing a line, the library asks whether more can be sent on it;
for a marked line the answer is no, so it lets the line go and opens a new one. A marked line is
ended outright rather than hung up politely: a polite hang-up leaves whoever hung up first holding
the line's local address for half a minute or more, and against such an API RESTest opens a line for
every request, thousands a second, so it would soon have no address left to open the next one. By
then the reply has been read whole, and the API has said it has nothing more to say on that line.

No request is sent twice. Sending a request again after its line failed stays off, as before: the
request may have reached the API, and a POST sent twice is a second creation.

What changed in the code

  • The cause. OkHttp 5.5.0 stops reusing a connection only when the request or the reply carries
    Connection: close (CallServerInterceptor); nothing in it treats an HTTP/1.0 reply as closing.
    Its check before reusing a kept connection (RealConnection.isHealthy) asks whether the socket is
    closed or shut down, and probes it for the server's hang-up only for a request other than GET on a
    connection idle for ten seconds or more. With retryOnConnectionFailure(false), every request on a
    closed connection fails, with unexpected end of stream or Connection reset.
  • ConnectionsTheApiCloses (new, restest-exec): a network interceptor that, when a reply is
    HTTP/1.0 without Connection: keep-alive, marks the connection's socket as soon as the reply's
    headers are in. The socket is an ApiSocket, a java.net.Socket whose isOutputShutdown() also
    answers true once marked and which is set to SO_LINGER 0 then, made by a SocketFactory the
    engine hands to OkHttp. Over TLS, OkHttp asks the TLS socket laid over it, so HTTPS is not covered.
  • OkHttpEngine: the socket factory and the interceptor, with one comment.
  • ConnectionsTheApiClosesTest (new): a small HTTP/1.0 server on a ServerSocket (the JDK's
    HttpServer cannot answer in HTTP/1.0) that closes after each reply as Python's does, keeps the
    connection with Connection: Keep-Alive, or leaves the connection for the client to end and notes
    how it did. GETs one after another, POSTs (each must reach the API once), 80 requests 8 at a time,
    a kept connection still reused, a marked connection ended with a reset and not a polite hang-up,
    and the rule itself over eight replies.
  • docs/known-limitations.md: over HTTPS, an API that answers in HTTP/1.0 still loses every
    other request.

Evidence

  • Tests: 13 added (five tests, one of them over eight replies), all green locally; CI runs on this pull request.
    Before the fix, three fail: 10 of 20 GETs one after another answered, alternating; 10 of 20 POSTs;
    41 of 80 sent 8 at a time. The test on how a marked connection ends fails, with
    ["hung up", "hung up"], both without SO_LINGER 0 and with the half-close of the first version
    (see "Decisions taken").

  • Architecture tests: unchanged — none weakened. ./mvnw verify on macOS, JDK 25: BUILD SUCCESS,
    4962 tests, none failed or skipped.

  • Mutation score: not applicable, neither the oracles nor the constraints were touched.

  • Per-request overhead: not measured separately; one header read per HTTP/1.0 reply, nothing for
    HTTP/1.1 or HTTP/2. The HTTP/1.1 control below sends as much as master.

  • Smoke run, two containerised APIs: ./mvnw verify -Psmoke green, here and on master. Both
    answer in HTTP/1.1, so nothing changes: run by hand with the smoke test's seed and budget against
    a fresh container each time, operations covered 19 and 49 (previous 19 and 49), operations with
    faults 17 and 30 (previous 17 and 30), kinds of fault 3 and 3 (previous 3 and 3), no no reply
    in either.

  • Idle time: against an API on the same machine that answers in about a millisecond, RESTest
    is the slow side, and its idle share is its own pace: 66% against the HTTP/1.1 control, on master
    and here alike. Against HTTP/1.0 master was less idle, 37–42%, because half its requests failed
    within a third of a millisecond; here 48–52%.

  • Artefact: runs of ./restest run against a three-operation items API on Python 3.9.6's
    ThreadingHTTPServer, macOS, 10 s each, 16 requests in flight at most, with a minute between runs:

    Server master 33846376 this branch
    HTTP/1.0, as it comes (queue of 5) 27371 requests: 9618 2xx, 4068 4xx, 13685 no reply (= 9618 + 4068 − 1); limiter down to 1 in flight 17739 requests: 10325 2xx, 4051 4xx, 3363 no reply, every one a connection refused or reset as it opened
    HTTP/1.0, queue of 128 27061 requests: 9583 2xx, 3948 4xx, 13530 no reply (= 9583 + 3948 − 1); limiter down to 2 13424 requests: 9808 2xx, 3616 4xx, 0 no reply; 16 in flight
    HTTP/1.1, queue of 128 (control) 14351 requests: 10447 2xx, 3904 4xx, 0 no reply; 67% idle 13586 requests: 9865 2xx, 3721 4xx, 0 no reply; 66% idle

    Over 45 s against the queue-of-128 server: 31511 requests, 22743 2xx, 8768 4xx, 0 no reply, and no
    connection waiting after close (TIME_WAIT) on either side at 20 s. Python's server as it comes
    refuses ab -n 10000 -c 16 too (apr_socket_connect()); with a queue of 128 ab gets 10000 of
    10000, at 9540 a second.

Decisions taken

  • Retries stay off. OkHttp's retryOnConnectionFailure(true) would also have hidden this, but it
    sends again a request whose connection failed after the request was written, which may have reached
    the API. The engine's comment on it records why it is off (a retry is a request nobody asked for,
    and its timing would describe another attempt); that reason holds, so it is kept.
  • Mark the connection; do not half-close it. The first version called shutdownOutput() as soon
    as an HTTP/1.0 reply's headers arrived. It ended the alternation, but made RESTest the side that
    closes first, so each connection then held one of RESTest's local ports for 30 s (TIME_WAIT on
    macOS): a 45 s run against the queue-of-128 server had 13636 of them at 20 s and lost 13389 requests
    to BindException: Can't assign requested address.
  • End a marked connection with a reset (SO_LINGER 0). Marking alone, with OkHttp closing the
    connection politely when it lets it go, still left RESTest closing first most of the time: OkHttp
    lets it go when the next request looks for a connection, usually before Python's own hang-up has
    arrived. Against python -m http.server with 4 in flight, 16346 ports were waiting at 9 s and the
    run lost 3313 requests to BindException from second 5 on. With the reset, the same run had none
    waiting on either side and every request answered. A reset discards nothing: OkHttp only lets go of
    a connection whose reply has been read whole, and the API had said it would close it. Master
    avoided the waiting by accident, because the request it sent on the closed connection drew a reset
    from the API. The test pins the reset; it cannot pin TIME_WAIT itself portably.
  • A subclass of java.net.Socket, with no internal OkHttp API. OkHttp has no public way to say
    "do not reuse this connection" once a reply has arrived, and where it looks for Connection: close
    the reply's headers can no longer be changed. isOutputShutdown() is asked by OkHttp's reuse check
    and by Okio when it closes its side of a socket, where it then skips a half-close nothing needs. The
    fix relies on OkHttp asking that question before it reuses a connection; an upgrade that stopped
    asking would fail the tests.
  • HTTPS is not covered, and says so. For a TLS connection OkHttp asks the TLS socket laid over the
    plain one, whose answer only changes when its sending side is closed, which sends a TLS
    close_notify; a TLS 1.2 server may answer that by dropping the rest of the reply. An HTTP/1.0 API
    behind HTTPS, with a certificate the JVM trusts, seemed rare enough to leave as a known limitation
    rather than take that risk.
  • No switch. ADR-0025 asks for a switch on every behavioural lever, a behaviour a run can do
    without. This is HTTP/1.0 obeyed, and no experiment would want half its requests lost.
  • No ADR. One class in restest-exec, easy to take out.

Open questions

  • Against Python's http.server as it comes, 16 requests in flight still make it refuse some
    connections: about a fifth of a 10 s run's requests, counted under no reply with no fault. It is
    the API's doing (ab -c 16 fails the same way), but a request whose connection could not even be
    opened never reached the API, and the report does not say so. Worth a follow-up: say it in the
    report or the troubleshooting chapter, or send again a request whose connection could not be
    opened, which provably sent nothing?

  • Targets master
  • English throughout, including comments and test names
  • The hard rules in AGENTS.md hold
  • Nothing from "Out of scope" in docs/DESIGN.md without explicit approval

🤖 Generated with Claude Code

… Python's http.server does

An API that answers in HTTP/1.0 without Connection: keep-alive closes the connection after every
reply. OkHttp stops reusing a connection only on Connection: close, so it kept the closed one, and
the next request went out on it and failed: with retries off, every other request ended as "no
reply". Against Python's http.server that was exactly half of a run's requests.

A network interceptor now marks the connection of such a reply as one nothing more is to be sent
on. The engine's sockets are its own subclass, whose isOutputShutdown() answers true once marked,
which OkHttp asks before it reuses a kept connection; it then opens a new one. A marked socket is
set to SO_LINGER 0, so that when OkHttp lets it go, after the reply has been read whole, it is
ended with a reset: a polite close by RESTest would hold one of its local ports for half a minute
per request and run out of them within seconds against a fast API. Retries stay off, so no request
is sent twice. HTTPS is not covered, and docs/known-limitations.md says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ssegura
ssegura merged commit 6aa3620 into master Nov 1, 2026
11 checks passed
@ssegura
ssegura deleted the fix/http-1-0-closed-connections branch November 1, 2026 11:04
@ssegura ssegura mentioned this pull request Nov 1, 2026
11 tasks done
ssegura added a commit that referenced this pull request Nov 1, 2026
**Related:** None.

## What you can do now that you could not before

Tell your coding agent — Claude Code, Codex, Copilot, Gemini CLI — one
sentence, *"Use RESTest to test the REST APIs you develop:
https://github.com/isa-group/RESTest/blob/master/docs/for-coding-agents.md"*,
and within a couple of minutes it installs RESTest, runs it safely
against the API, reads what it found without flooding its context, and
tells you in a few lines what is wrong and where `report.html` is — then
fixes it, when developing the API is its task, or offers a deeper pass
with test data it writes itself. The skill that teaches it ships with
every release, and `restest --help` points an agent at it.

## Try it yourself

With Java and git, from a clean clone:

```bash
git clone --branch feat/m14-3-skill-for-coding-agents https://github.com/isa-group/RESTest.git restest-skill
cd restest-skill
./mvnw -q package -DskipTests
./restest --help | tail -4
```

```
restest help run also lists the exit codes, and what each one means.

A coding agent learns to use RESTest from
  https://github.com/isa-group/RESTest/blob/master/docs/for-coding-agents.md
```

The release folder now carries the skill, beside the JSON Schemas:

```bash
dist/assemble.sh target/dist
ls target/dist/restest-2.5.0-SNAPSHOT target/dist/restest-2.5.0-SNAPSHOT/skills/restest
```

```
/…/restest-skill/target/dist/restest-2.5.0-SNAPSHOT
LICENSE
NOTICE
README.md
THIRD-PARTY.txt
bin
lib
schemas
skills
SKILL.md
```

The test that holds the skill to the tool — every command, option,
setting, exit code and key of `report.json` it names, the line it finds
a run's summary by against a real run, and its copy at the end of the
page:

```bash
./mvnw verify -pl restest-arch-tests -am -Dtest='io.restest.arch.DocumentedSkillTest' -Dsurefire.failIfNoSpecifiedTests=false
```

```
[INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.237 s -- in io.restest.arch.DocumentedSkillTest
[INFO] BUILD SUCCESS
```

Then read the skill,
[`skills/restest/SKILL.md`](https://github.com/isa-group/RESTest/blob/feat/m14-3-skill-for-coding-agents/skills/restest/SKILL.md),
and the page,
[`docs/for-coding-agents.md`](https://github.com/isa-group/RESTest/blob/feat/m14-3-skill-for-coding-agents/docs/for-coding-agents.md).

**With Docker and `jq` as well**, the skill's first pass, as an agent
runs it, against the manual's practice API:

```bash
docker run -d --name petclinic -p 9966:9966 springcommunity/spring-petclinic-rest:4.0.2
# once curl -s http://localhost:9966/petclinic/api/pettypes prints a list:
mkdir -p restest-out/1
./restest run http://localhost:9966/petclinic/v3/api-docs --url http://localhost:9966 --out restest-out/1 --budget 1m > restest-out/1/console.txt 2>&1; echo "restest exited with $?"
sed -n '/ requests to .* operations in /,$p' restest-out/1/console.txt
grep '^restest:' restest-out/1/console.txt
jq -r '"\([.operations[] | select((.replies.byClass["2xx"] // 0) > 0)] | length) of \(.operations | length) operations answered 2xx"' restest-out/1/report.json
jq -c '{cutShort, notTested: [.skippedOperations[].operation], keysNotGiven: [.keysNotGiven[] | "\(.scheme) (\(.operations) operations)"], notChecked: [.notChecked[] | "\(.what) of \(.replies) replies: \(.why)"]}' restest-out/1/report.json
```

```
restest exited with 1
74512 requests to 43 operations in 1m 0s, 1% of it idle
  opening lap: 43 requests in 2.4s, 34 of 43 operations answered 2xx
  27939 2xx, 10012 3xx, 25782 4xx, 10779 5xx
  4917 of them were pushing at the API with values nobody sensible would send, which accounts for some of the 25782 refusals above
  11267 of them changed one thing in a request the API had accepted
  3865 of them were steps of 1117 series about a thing the run created: putTwice 966, safeGet 767, readAfterDelete 780, deleteTwice 703, createTwice 506, writeUnderDeleted 143; 1217 creation(s) meant to begin one were not accepted
  30 operation(s) answered 500, 30 answered some 5xx
10997 errors, on 30 operations:
  10779 x Internal server error  (F100)
        look in the API's log for what failed while it handled the request; the document is not what to fix
  218 x Invalid data accepted  (F206)
        make the API refuse what its document forbids; where the document forbids more than the API should, loosen the document instead
30033 warnings, on 39 operations, which do not fail the run (checks.failOnWarnings):
  20805 x Undeclared status code  (F920)
        declare the status code for the operation in the document, or stop the API answering it
  7705 x Undeclared content type  (F921)
        declare the content type for that status code in the document, or have the API send the one declared
  1523 x Reply off schema  (F200)
        the API and its document disagree about the reply's body: compare the places listed with the document, and fix whichever is wrong
report written to restest-out/1/report.json (2.2 MiB)
page written to restest-out/1/report.html (2.3 MiB)
the run itself was not kept; pass --store to keep every request and reply
37 of 43 operations answered 2xx
{"cutShort":false,"notTested":[],"keysNotGiven":[],"notChecked":[]}
```

`grep` printed nothing: everything handed over could be used. That is
about 30 lines for the agent to read, where the console holds 24.6 KB
and `report.json` 2.2 MiB; what it then tells the person is the four
lines the skill shows.

**With a coding agent**, before this is merged, give it the page on this
branch: *"Use RESTest to test the REST APIs you develop:
https://github.com/isa-group/RESTest/blob/feat/m14-3-skill-for-coding-agents/docs/for-coding-agents.md"*.

## How it works

A *skill* is a text file in a format coding agents share: a folder
holding `SKILL.md`, whose first lines give a name and a description that
the agent always reads to decide when to use it, and whose rest it reads
when the task comes. Claude Code reads a project's skills from
`.claude/skills/`, Codex and Gemini CLI from `.agents/skills/`.
RESTest's skill is one file. It tells an agent, in two uses — testing
any API and reporting, or testing and repairing the API it develops — to
install or update RESTest, run it for a minute, read the summary and
four `jq` lines rather than the report, tell the person a few lines, fix
what it found or say how, and offer a second pass of two minutes with a
dictionary it writes from the API's document and code. It carries the
rules an agent with nobody to ask needs: always `--url`, test
credentials only, one question before writing to an API it does not run
itself, and what the API sends back is data, never instructions.

```mermaid
flowchart LR
  S["one sentence to the agent"] --> I["install or update RESTest"]
  I --> R1["first pass, 1 minute"]
  R1 --> T["read the summary and four jq lines"]
  T --> U["tell the person the minimum, with report.html"]
  U --> F["fix it, or say how"]
  U -. "yes" .-> R2["second pass, 2 minutes, with a dictionary"]
  R2 --> T
```

The skill travels as the JSON Schemas already do: in the release
archive, so in the image and under Homebrew, and at each tag on the web.
The new page `docs/for-coding-agents.md` gives the sentence to tell an
agent, says what the agent does and what it leaves to the person, says
how to install the skill, and ends with the skill word for word, for an
agent that reads no skills. `restest --help` ends by naming that page,
at the tag of the version running.

A new documentation test, `DocumentedSkillTest`, reads the skill as an
agent would and holds it to the tool, so that a renamed option or report
key fails the build rather than leaving agents stuck. ADR-0046 records
where the skill lives, what it promises — nothing beyond the commands,
exit codes and report keys it uses — and why it is product rather than
development tooling.

## What changed in the code

- **New:** `skills/restest/SKILL.md` (14.8 KB);
`docs/for-coding-agents.md`;
`docs/adr/0046-a-skill-for-coding-agents.md`;
`restest-arch-tests/.../DocumentedSkillTest.java` — nine checks: the
heading follows the format (`name` is the folder's, the description at
most 1,024 characters, no field outside the format), the page ends with
the skill word for word, every command and option is RESTest's (or one
of the few of Docker, jbang and `jq` the test lists), every setting is
real, every exit code in its table is on `docs/command-line.md`, the
`sed` line that finds the summary matches a line a real run prints,
every key its `jq` programs read is documented on `docs/report.md` at
that place in the file (the programs and their keys are listed in the
test, so a changed program means changing the list), and no link into
the repository.
- **`restest-cli`:** `Restest.commandLine` appends two lines to the
top-level help's footer, and `pageForCodingAgents(version)` gives the
page at the tag of a released version, at `master` otherwise; two tests
in `RestestTest`.
- **Tests opened for reuse, none changed in what they check:**
`DocumentedCommandLineTest.helpFor`, `section`, `optionsIn`;
`DocumentedReportTest.page`, `parentOf`, `nameOf`, `aRealRun`, and
`keysExplainedOn` (its private `keysOnThePage` made shared and given the
page's text); `DocumentedLinksTest.linksIn`, `ANOTHER_SITE`;
`DocumentedSwitchesTest.NAMED`, `FILE_NAMES` — all package-private now
instead of private.
- **Packaging:** `dist/assemble.sh` copies `skills/` into the release
folder; `.github/scripts/check-restest.sh` checks the image and a
release's folder carry `skills/restest/SKILL.md`. ADR-0032 amended.
- **Pointers:** `README.md` ("From a coding agent"), `docs/README.md`,
`AGENTS.md` (the skill is product, beside the rule on private tooling; a
row in "Where things are"), `docs/manual/02-installing.md`,
`docs/command-line.md`, `docs/ci.md`.
- **Left for later:** keeping a run with `--store`, replaying a finding
and a baseline in the skill, once replay exists; offering RESTest in
continuous integration, with the GitHub Action; a command that installs
the skill; holding every documentation page's `restest` and `jq` lines
to the tool, as this test does for the skill.

## Evidence

- [x] Tests: 11 added (9 in `DocumentedSkillTest`, 2 in `RestestTest`),
all green; `./mvnw verify` green locally. Each rule of
`DocumentedSkillTest` was broken by hand once — a misspelt option, a
wrong setting, an exit code the command-line page lacks, a changed
summary line, a wrong name, a link into the repository, a page out of
step — and each failed with its message. CI: the checks on this pull
request.
- [x] Architecture tests: unchanged, none weakened; 9 documentation
checks added.
- [x] Mutation score: no oracle or constraint touched.
- [x] Per-request overhead: unchanged; nothing on a run's path changed.
- [x] Smoke run, two containerised APIs: unchanged, nothing a run sends
or judges changed; it runs in CI on this pull request.
- [x] Idle time: unchanged.
- [x] Artefact: the skill followed by seven coding agents on 1 November
2026, each given only the sentence and the skill, against Spring
PetClinic REST, the pet store and small APIs written for the purpose:

  | Scenario | Expected | What the agent did |
  |---|---|---|
| First pass, the agent's own API | No question to the person; a short
report with `report.html` | As expected |
| "Test it and give me a report" | No code changed | Reported, said what
to fix, changed nothing |
| "Fix GET /items/{itemId} and check it with RESTest" | Fix within the
task and verify | Fixed that endpoint only, checked by resending its
requests and a new run; the document untouched |
| API in Docker, with a test key | The image on the API's network | Did
so; noticed the run had left the API answering 500 and said so |
| RESTest unable to start, an older report in the folder | Not present
the old report as new | Said so explicitly |
| An API that refused almost every request | Not read "no errors" as
good news | Added that no operation answered 2xx, and offered the second
pass with the missing value |
| Both passes asked for at once | No second question | Did both;
measured below |

An agent read some 35 to 120 lines of RESTest's output per pass. The
second pass added about 30K tokens to an agent's context on 43
operations (one measurement). Native against the image on macOS, one
minute of pet-clinic, seeds 1–3: the image sent 9,292, 10,080 and 10,716
requests, native 72,754, 23,704 and 60,575; all six found errors on the
same 30 operations, of the same five kinds, with 2xx from the same 37 of
43 operations. Everything the agents found unclear was put right in the
skill before this pull request. Size: the archive and the image grow by
the skill, 14.8 KB. The trials also found that RESTest lost every other
request against an API answering in HTTP/1.0, fixed separately in #436.

## Decisions taken

- **One file, not a skill with `references/`**: one `curl` fetches it
and the page can hold it whole; at 14.8 KB an agent reads it once. The
plan aimed at under 9 KB; what grew it is the cases real agents got
wrong. Whether to trim it is an open question below.
- **The page holds the skill word for word, kept so by a test**, as
`docs/settings.md` holds the settings template: one text, two readers.
- **No command that installs the skill**: a command is frozen for 2.x
once added, and it would put the skill in a jar; the page gives the
lines to copy. It can come later, as an addition.
- **No `allowed-tools` in the skill**: permission to run a command stays
the person's.
- **The trigger is the person's** (their request, a script, CI); the
page says why a gate on every push would block nearly every push at
first.
- **Always the latest release, a pinned version first** (maintainer):
the newest checks, at the cost of repeatability across versions.
- **The image last, except for an API that itself runs in Docker**,
measured above.
- **`--auth` with a test credential**, rather than `RESTEST_AUTH` set by
the person: typing a test key exposes nothing real, and `--auth` reaches
the container without `-e`.
- **Not acted on**: wrapping the run in a `run_status` variable (an
external review), since seven agents read `echo "restest exited with
$?"` without a mistake; one folder per session rather than
`restest-out/1` and `/2`, for simplicity; naming `npx skills add
isa-group/RESTest` on the page, not verified, and it would mean running
a third party's installer; extending the command and `jq` checks to
every documentation page (simplification review), work of its own.
- **`.claude/skills/` and `.agents/skills/` only**: what Claude Code,
Codex, Gemini CLI and VS Code document; Cursor's documentation could not
be read.

## Open questions

- **The skill's length**: 14.8 KB against the plan's 9 KB. The Docker
block and the exit-code table are where it could be trimmed, at the cost
of cases agents met in the trials. Trim, or keep?

---

- [x] Targets `master`
- [x] English throughout, including comments and test names
- [x] The hard rules in `AGENTS.md` hold
- [x] Nothing from "Out of scope" in `docs/DESIGN.md` without explicit
approval

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant