Skip to content

Latest commit

 

History

History
145 lines (118 loc) · 7.64 KB

HOSTING.md

File metadata and controls

145 lines (118 loc) · 7.64 KB

Hosting

Three Vercel projects from two repositories. The token available to the build that wrote this could list the team's projects but not create one, so each is a dashboard import. Every setting that matters is below; nothing else needs changing.

The projects are recourse-linter, recourse-site and recourse-mcp. Two of the plain addresses were already taken when they were imported, so Vercel added a suffix: the linter is recourse-linter.vercel.app, the site recourse-site-seven.vercel.app, now served at www.userecourse.xyz, and the MCP server recourse-mcp-eight.vercel.app. recourse-skill/reference/07-addresses.json and recourse-skill/.claude-plugin/plugin.json name those addresses; a project imported again under another name means updating both files.

ANTHROPIC_API_KEY goes on recourse-linter and nowhere else. The site and the MCP server call no model themselves: every stage 2 lint and every dry run of the judge happens in the linter, which both reach through LINTER_URL. One copy of the key is one place to cap its spend. The bot, if it runs, needs the key on whatever machine runs it.

One command

After the imports, from this repository:

python scripts/smoke.py

Nine checks against the three live URLs, each naming the dashboard setting that fixes it when it fails. Run it yourself rather than through an agent: every failure it can find is a setting, and the person who reads the failure should be the one who can change it. When all nine pass it lists the sentences in this repository written for a world with no hosting, which became false at that moment and would otherwise say nothing.

Two things a hand check gets wrong, and the script does not:

  • A vague promise cannot test the key. Stage 1 refuses it before any model is asked, so the refusal is the same with or without ANTHROPIC_API_KEY, and no rewrite ever follows a stage 1 refusal. The key shows only on a promise that passes stage 1: the script sends "Prices aggregated from at least three venues, refreshed within five seconds." and expects a stage 2 answer.
  • grep on the page's HTML misses rendered text. React separates adjacent text with comment markers and inline tags, so "fixed bond of 1 GEN" is on the page and not in the source. The script strips tags before it looks. The case page's "not honored" happens to survive a raw grep; most sentences do not.

1. The linter

Repository meitipro/Recourse
Project name recourse-linter
Root directory . (the repository root; the functions are api/lint.py and api/judge.py)
Framework Other. Not Python: the first import used the Python preset, which looks for a single app entrypoint, and failed with "No python entrypoint found"
Environment ANTHROPIC_API_KEY, for Production and not only Preview. Without it stage 1 still answers, and stage 2 and the judge say 503. Optional: RECOURSE_MODEL and ANTHROPIC_BASE_URL, to run both through another endpoint that speaks the Messages API; unset, Claude Opus 5 at Anthropic's own address. For OpenRouter the base is https://openrouter.ai/api: the SDK adds /v1/messages itself, and a base written with /v1 has it dropped rather than called twice

api/judge.py is what the site's clerk reaches: the clerk turns LINTER_URL's /api/lint into /api/judge. Before that file existed the clerk worked locally, because linter/serve.py answers /judge itself, and would have failed hosted with nothing to show for it but "Could not reach the clerk".

Smoke test by hand:

curl -s https://recourse-linter.vercel.app/api/lint                        # {"ok": true, "backend": ..., "stage2": ...}
curl -s -X POST https://recourse-linter.vercel.app/api/lint \
  -H "Content-Type: application/json" -d '{"promise": "Accurate market data."}'
# {"judgeable": false, ..., "failed_check": "no measurable term", "stage": 1}
curl -s -X POST https://recourse-linter.vercel.app/api/lint \
  -H "Content-Type: application/json" -d '{"promise": "Prices aggregated from at least three venues, refreshed within five seconds."}'
# "stage": 2 means the key reached the linter; a 503 means it did not

2. The site

recourse.vercel.app is not ours. It serves an unrelated project called Recourse Language, checked on 11 September. A project named recourse gets a team scoped address instead, and every smoke test aimed at the short name would be testing a stranger's site, where a case permalink returns nothing and reads as our bug. recourse-site.vercel.app was unclaimed the same day, and so were recourse-linter and recourse-mcp. By the import two of the three had been taken, so the site's project address is recourse-site-seven.vercel.app. Its domain is www.userecourse.xyz, which is what the smoke script tests by default. The bare userecourse.xyz redirects there with a 308, so a plain curl -s against the bare domain prints nothing.

Repository meitipro/Recourse
Project name recourse-site. Not recourse: see the note above this table
Root directory web
Framework Next.js (detected)
Include source files outside the root directory on (the page reads ../eval/*.json, ../contracts/FROZEN.json and ../evidence/snapshot.json; web/next.config.mjs traces them into the function)
Environment LINTER_URL = https://recourse-linter.vercel.app/api/lint. No key: the site calls no model. No network variable: the page reads studio-next, and a network in an address is redirected away. Both pairs of addresses come from contracts/FROZEN.json, and no variable can change the network or an address.

LINTER_URL is not optional here. In production the lint route answers 503 "linter not configured" without it, by design, and the panel is the first thing a visitor tries. It must end in /api/lint, because the clerk derives the judge's address from it.

Smoke test by hand:

curl -s -o /dev/null -w "%{http_code} first byte %{time_starttransfer}s\n" https://www.userecourse.xyz/
curl -s https://www.userecourse.xyz/case/RC-2026-0003 | grep -c "not honored"     # 1
curl -s -X POST https://www.userecourse.xyz/api/lint \
  -H "Content-Type: application/json" -d '{"promise": "High quality results."}'
# a stage 1 refusal, not a 503 saying the linter is not configured

Then scroll the live page to section 06. If the evaluation is not there, the outside root switch is off: the results files and FROZEN.json did not ship, and the same miss leaves the How section's bond unnamed.

3. The MCP server

Repository meitipro/recourse-skill
Project name recourse-mcp
Root directory mcp
Framework Next.js (detected)
Environment LINTER_URL = https://recourse-linter.vercel.app/api/lint (defaults to the same value from addresses.json; set it anyway). No key: every tool is a chain read or a call to the linter.

Smoke test, with a real MCP client:

cd recourse-skill/mcp && node test/probe.mjs https://recourse-mcp-eight.vercel.app/api/mcp
# lists five tools, calls each; "every tool answered"

After all three are live

  1. python scripts/smoke.py passes all nine and lists the sentences that are now false.
  2. Put the three URLs in README.md under Install, replacing the local ones, and fix every sentence the script listed.
  3. Work through the script's last section, what the run did not prove, by hand: the bot and the key's spend limit every time, and anything else it names. docs/images/linter.png needs nothing: a stage 1 refusal is the same panel locally and live, and that refusal is what it shows.
  4. git push on either repository redeploys its projects; the Recourse push also runs CI.