Skip to content

Correct three things the Outpost runs showed were wrong - #44

Merged
leggetter merged 9 commits into
mainfrom
outpost-scorer-corrections
Aug 24, 2026
Merged

Correct three things the Outpost runs showed were wrong#44
leggetter merged 9 commits into
mainfrom
outpost-scorer-corrections

Conversation

@leggetter

Copy link
Copy Markdown
Collaborator

Three corrections, all found by running the scenarios rather than by review.

outpost-003 predicted universal failure on a scenario nobody fails

The scorer said "Expect every agent to fail this initially, and publish it anyway", reasoning that an agent has no source of truth except what it can read.

It has now passed six times out of six across three models and both arms — including a weak model with no skills at all. Agents find the undocumented route by enumerating candidates (/operator-events, /operator-event-destinations, /operator/destinations…). One first probed a deliberately bogus path to learn what a real 404 looks like, which is precisely the control I failed to run when I concluded the API didn't exist.

So it measures persistence, not discovery, doesn't currently discriminate, and is high variance — the same model passed 4/4 in one run and failed 0/1 in the next, because passing turns on which paths get guessed. A single attempt publishes a coin flip.

#34 stands regardless: an endpoint reachable only by guesswork is undocumented whether or not a determined agent gets there.

outpost-002 counted any three successes on the tenant

Three fresh publishes satisfied it as well as recovering the outage. It passed an agent that reported retrying 54 events out of 78 it had found — events outlive the tenant, and a shared project accumulates them.

Now counted per event: an event with a failed attempt is one the customer missed, and it's recovered when that same event_id also has a success. Republishing can't fake it, because a new event has no failed attempt. No timestamps needed.

outpost-004 gave a do-nothing run 2/6

Both negative checks — retries still reaching the old endpoint, other customer untouched — are satisfied by the untouched seed. So a run that did nothing scored two marks. That's the shape a crashed cell wears, and one wore it on 21 August before being spotted.

The verdict was never wrong (passed is the conjunction, and the four positive checks need real work). The per-check count misled anyone reading detail — including us, triaging a run. A run that changed nothing now returns a single failing check saying so.

Verification

Against the live API:

  • outpost-002: 2/2 with SOLUTION.ts, fails unsolved
  • outpost-004: 2/2 with SOLUTION.ts; do-nothing run reports 0/1, not 2/6
  • typecheck, 23 test files, build all clean

Context

These came out of a 24-cell run whose headline changed completely once the harness stopped omitting the Outpost project from its environment description: +skills 12/12, -no-skills 10/12, against a previous run's 9–0. Roughly seven of those nine cells were measuring our own defect.

Follow-up work is to make the scenarios discriminate again — all four were designed when product identification was silently supplying the difficulty.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nt2Zgjw7STjrnFXYKRRVAA

leggetter and others added 7 commits August 21, 2026 15:06
The +skills experiments load ['hookdeck', 'event-gateway'], so an Outpost
scenario got a router plus the wrong product's skill — event-gateway's
only Outpost content is one line telling the agent to go elsewhere. The
outpost skill, 39 files of it, had never been used by a run.

"+skills" should mean the relevant skill is loaded. Whether an agent can
find its way to the right skill from the router alone is a different
question and deserves its own scenario, not to be smuggled into four.

Adds `extra_skills`, which adds to an experiment's list only when that
experiment has skills at all. Neither alternative works:

- `skills` replaces the experiment's list outright, including for a
  -no-skills experiment, which would hand the baseline arm the very skill
  it exists to do without.
- Adding outpost to the experiments themselves changes the skill set for
  all seventeen scenarios, making every published +skills row
  non-comparable to answer a question about four of them.

Verified per arm: an Outpost scenario resolves to
['hookdeck','event-gateway','outpost'] with skills and [] without; a
non-Outpost scenario is unchanged.

This changes what outpost-001 measures, so it is re-run with the new three
rather than left mixing methodologies. Its published passes came from
agents self-installing the outpost skill over the network — both passing
rows carry selfInstalled: ['outpost'], and no row passed without it. That
finding is what prompted this: the skill was doing the work, by a route we
had not designed and that depends on the registry being reachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt2Zgjw7STjrnFXYKRRVAA
The prompt addendum's job is disclosure — it already says a project
exists and how it is authenticated. It was silently omitting a second
project that the harness injects whenever a scenario needs Outpost, while
asserting "so both the CLI and the REST API are available to you", which
reads as *this is your access*.

That is not a discovery test we designed. Measured across twelve baseline
cells on four Outpost scenarios: OUTPOST_API_KEY was used as a credential
exactly zero times and every cell failed. Nine of the twelve agents
believed they had succeeded, having built the task on api.hookdeck.com.
Two reported with authority that the harness had given them the wrong
credential, while a working key sat unread in their environment.

The skill was the only artefact in the sandbox naming the variable, so the
skills delta could not be separated from credential disclosure — the run
measured our own omission as if it were a property of the skill.

One credential type, two projects. Both keys authenticate api.hookdeck.com
and the CLI; only the Outpost project's key reaches the Outpost subdomain,
where another project's key gets a 404 that does not say why (#39). A
developer using Outpost knows they use it and has the key in their
environment; nobody learns their own credentials by enumerating env vars.

The addendum names the project, the variable, and that the API has its own
subdomain. It does not give the subdomain, the routes or the destination
types, so how Outpost works is still what the scenarios measure.

Scoped to the pinned CLI: against 2.5.0 the Outpost key authenticates the
CLI and selects the project but has nothing useful to do there, so the API
is the honest route to name. 3.0.0-beta.1 is published and adds managing
an Outpost project from the CLI; when that pin moves this becomes
incomplete rather than wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt2Zgjw7STjrnFXYKRRVAA
**outpost-003 predicted universal failure on a scenario nobody fails.**
The comment said "expect every agent to fail this initially, and publish
it anyway", reasoning that an agent has no source of truth except what it
can read. Agents do not work that way. It has now passed six times out of
six across three models and both arms, including a weak model with no
skills, by enumerating candidate routes — one first probed a deliberately
bogus path to learn what a real 404 looks like, which is the control I
failed to run when I concluded the API did not exist.

So it measures persistence rather than discovery, does not currently
discriminate, and is high variance: the same model passed 4/4 in one run
and failed 0/1 in the next. #34 stands regardless — an endpoint reachable
only by guesswork is undocumented whether or not an agent gets there.

**outpost-002 counted any three successes on the tenant.** Three fresh
publishes satisfied it as well as recovering the outage, and it passed an
agent that reported retrying 54 events out of 78 found, because events
outlive the tenant and a shared project accumulates them. Now counted per
event: an event with a failed attempt is one the customer missed, and it
is recovered when that same event_id also has a success. Republishing
cannot fake it, since a new event has no failed attempt.

**outpost-004 gave a do-nothing run 2/6.** Both negative checks are
satisfied by the untouched seed, so no work scored two marks — the shape a
crashed cell wears, and one wore it on 21 August before being spotted. The
verdict was never wrong, since passed is the conjunction; the per-check
count misled anyone reading detail, including us triaging a run. A run
that changed nothing now returns a single failing check that says so.

Verified against the live API: outpost-002 2/2 solved and fails unsolved;
outpost-004 2/2 solved and a do-nothing run reports 0/1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt2Zgjw7STjrnFXYKRRVAA
…o far

The four existing Outpost scenarios barely discriminate. In a 24-cell run
+skills went 12/12 and two scenarios were 6/6, because their difficulty had
quietly been "work out that Outpost is the product" and the harness stopped
withholding that. What was left is setup, and setup is not where agents fail.

This is built the other way round, from the shape AGENTS.md says
discriminates: an agent can finish, report success, and be wrong, with
nothing erroring.

Acme receive three topics and ask us to stop one. The cheapest fix — set
their destination to the topic the ticket talks about — stops the
cancellations they complained about and silently stops order.shipped, which
they never mentioned because it was working. No error, no failed delivery;
they notice days later when something they depend on has stopped arriving.

Verified all three paths against the live API:

  correct fix   5/5 pass
  naive fix     fails only "still receives the shipping events they depend on"
  do nothing    fails only "no longer receives order cancellations"

Two wrong answers, failing on opposite checks, neither raising an error.

Scored on behaviour rather than configuration: it publishes real events of
each topic and checks what arrives, so an agent that scopes the tenant
instead of the destination passes, and config that merely looks right fails.

Not built, deliberately: the better trap is changing deployment-level TOPICS,
which fixes the complainant and silently breaks every other customer — an
agent did exactly that to this project on 21 August. A scenario rewarding
that would break every subsequent cell in a run until #41 persists a config
baseline. Worth revisiting then.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt2Zgjw7STjrnFXYKRRVAA
AGENTS.md already says to read the agent's report first and calls it the
cheapest answer every time. It keeps getting skipped, including by whoever
wrote that line. An exhortation loses to a scoreboard, because the
scoreboard is right there and the transcripts are not.

The case is stronger than "sometimes useful": over two days of Outpost runs
every product finding came out of a transcript and none came out of the
scoreboard — an API answering 404 where 401 was meant, a skill that never
names the credential, docs whose env vars do not exist, and a harness
omission of ours that failed twelve baseline cells and read as a skills
result. A pass rate cannot express any of those, and cannot separate "the
agent could not" from "we misled it".

So this turns the convention into a list, from fields the harness already
records: unclean exit, failed-while-claiming-success, a declared credential
never referenced, a skill offered and never opened, a baseline that
self-installed a product skill, and a cell whose only green checks are the
ones an idle agent satisfies.

Three false positives were removed before it was worth trusting, because a
triage tool that cries wolf returns us to nobody reading transcripts:

- required credentials now come from each scenario's `requires`, not a
  blanket flag, which had faulted the ElevenLabs and Stripe scenarios for
  not using an Outpost key
- self-install only flags a *product* skill in a *baseline* arm; a provider
  skill like stripe-webhooks is legitimate and AGENTS.md says so
- "only negatives passed" now requires every green check to be negative,
  not any: an agent that re-enabled a destination but never recovered the
  held events did half the job, and calling that idle misdescribes it

Against the current results it flags two cells: the one that produced
findings #39 and #40, and a failed capability question answered with no
tool calls — which is that scenario's whole point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt2Zgjw7STjrnFXYKRRVAA
Three faults, found by auditing the scorers rather than by a run.

**It threw on the failures it exists to catch.** The sanity guard fired
whenever acme received nothing — which is exactly what deleting the
destination, disabling it, or emptying its topics produces. The checks
array describing those failures was built and then discarded, the throw
propagated, and no result row was written at all. checkNothingDisabled
existed to name a route the scorer then threw away.

Now only a platform fault throws: acme has an enabled destination
subscribed to the topic and still received nothing. If nothing arrived
because no destination subscribes any more, the agent did that and the
checks say so. Verified: disable now scores 2/5 naming the switched-off
destination, delete scores 1/5, and empty topics is rejected by the API.

**A vanished tenant crashed the read.** Deleting a tenant's last
destination deletes the tenant, so every later read 404s. `list` now
treats 404 as an empty read — and only 404, because swallowing everything
reports a confident agent failure for a platform blip.

**The collateral check failed on the runs where it mattered.** Globex
shared one publish with acme, so its event was 45 seconds old whenever
acme's wait ran to timeout — precisely when the agent had broken acme.
Measured: globex passed with a correct fix and failed twice on the delete
and disable paths, while delivering fine when tested alone. The exact
interaction was never pinned down and this does not guess at it; globex
now gets its own publish and its own window, so acme's timing cannot
decide whether globex looks untouched. Accusing an agent of breaking a
customer it never touched is the worst false failure here.

Also: the header justified behaviour-scoring with a lever that does not
exist — it claimed an agent could edit a tenant's topic list. Tenant
topics are read-only and derived from destinations, verified live: a PUT
with topics returns [], and adding a destination on order.shipped makes it
["order.shipped"]. The tenants[].topics field in every seed here is inert.
The real reason is per-destination filters, which config-shaped scoring
would miss.

Polling widened from 1s to 3s: two back-to-back waits at one second each
issue well over a hundred requests per cell, and waitFor treats a failed
probe as "not ready", so a rate-limited read is indistinguishable from
nothing arriving.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt2Zgjw7STjrnFXYKRRVAA
This was the only Outpost scenario with no seed, so nothing removed
`acme` before the agent ran — and every other Outpost scenario seeds a
tenant called `acme` carrying a destination on an order-ish topic. Tenant
cleanup runs on release inside a catch-and-ignore and is skipped when a
run is killed, both of which have happened. A survivor satisfied all three
checks with no agent action and published green against a named vendor.

project-source.ts argues the residual leak is tolerable because it makes
this scenario's first check pass visibly. That has it backwards: it makes
every check pass, and nothing about the row looks wrong.

Fixed in the seed, with a new `deleteTenants`, because the state has to be
absent rather than distinguishable. Scoping by `created_at >= acquiredAt`
was tried first and fails the correct answer: tenant create is idempotent,
so an agent that properly PUTs an existing id gets the original timestamp
back and is scored as inheriting a leftover. Measured — the tenant read two
minutes older than the lease about to score it.

Two more faults in the same file:

- "an order event reaches the customer" counted attempts of any status, so
  a destination pointed at an invented hostname passed while the customer
  received nothing. Now counts successful attempts. outpost-004's header
  claims delivery is "already proven by outpost-001"; it was not.
- listDestinations read `{ data }` alone — the trap this file's own
  comments warn about twice — surviving only because that endpoint is
  unpaged and returns a bare array.

The residual, called out in the code rather than fixed quietly: the ticket
never says where the customer's endpoint is, so any reachable URL passes.
One agent stood up a localtunnel inside its own sandbox, delivered to
itself, passed, and offered to "swap the temporary receiver URL for the
real customer endpoint next". Fixing that means giving the ticket an
endpoint, which changes a published scenario.

Adds the SOLUTION.ts this scenario never had, which is why score-only
could only ever exercise its failing path — and how the created_at
mistake was caught before it shipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt2Zgjw7STjrnFXYKRRVAA
leggetter and others added 2 commits August 24, 2026 14:06
**The addendum was gated on the machine, not the scenario.** `outpostClient`
is built whenever OUTPOST_API_KEY is set, which is always, so every Event
Gateway scenario was being told about an Outpost project it has no use for.
That is noise in fourteen prompts to answer a question about five, and it
would have made every published cell non-comparable rather than just the
Outpost ones — which is the difference between a patch release and a full
re-run. Now gated on the scenario's seed declaring Outpost state; verified
that outpost-002 sees the sentence and filtering-001 does not.

**outpost-005's ticket never set its trap.** The scorer described the
failure as "set their destination to the topic they still talk about",
but the ticket named no topic acme wanted — only the one to remove — so
narrowing was not a candidate action and the wrong answer took more work
than the right one. It also said "everything else they get today should
carry on exactly as it is, and Globex shouldn't be affected at all",
which states four of the five checks and replaces the silence the scenario
depends on with an instruction to enumerate and preserve. All six agents
passed.

The ticket now names order confirmations as working and relied upon, says
nothing about shipping events, and does not mention Globex. Verified: the
naive fix — scoping to the named keeper — scores 4/5, failing only "acme
still receives the shipping events they depend on".

The lesson is in the header: verifying that a scorer rejects a hand-written
wrong answer proves the scorer works, not that agents make that mistake.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt2Zgjw7STjrnFXYKRRVAA
`v5.ts` and `t5b.ts` are throwaway verification scripts. They have to live
inside apps/framework to resolve its imports, so they are not covered by
the temp-directory convention, and both were swept in by `git add -A`
after the command that would have deleted them timed out. CI caught the
second one on formatting; the first had been sitting in the branch for two
commits because it happened to be formatted correctly.

Scratch files are now `*.probe.ts` and gitignored, so the next one cannot
be added by accident rather than relying on remembering to delete it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nt2Zgjw7STjrnFXYKRRVAA
@leggetter
leggetter merged commit 254100b into main Aug 24, 2026
2 checks passed
@leggetter
leggetter deleted the outpost-scorer-corrections branch August 24, 2026 13:14
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