| name | neo-run-and-inspect |
| description | Tells an agent how to RUN a NeoHaskell app and work with its LIVE HTTP surface. Use whenever asked to start the app / run the server, hit or POST to an endpoint, read a query, view or download the OpenAPI spec, open the Scalar docs UI, open the event-modeling IDE, run neo inspect (commands/queries/domains/wiring) and interpret its output, or run neo inspect sync to refresh event-model.json from code. Also use before writing hurl e2e tests to confirm real endpoint paths and ports, and for health/readiness probes and port numbers. Grounds every URL, subcommand, and port in neo/src/cli.rs and Service/Transport/Web.hs. Do NOT use for the full `neo` subcommand/flag catalog or scaffolding/build/test flags (neo-cli), for writing the hurl files themselves (write- hurl-e2e), for registering endpoints in code (wire-feature), for where source files go (neohaskell-module-layout), or for validating event-model.json (verify-event-model). |
| metadata | {"model":"haiku"} |
This is NeoHaskell, not vanilla Haskell. Files share the .hs extension but the language uses import Core instead of Prelude, Task instead of IO, Array instead of lists, and |> instead of $. This skill is a tooling reference — it does not generate Haskell code.
Inputs / Outputs / Next
- Inputs: a task — start the app, hit an endpoint, view API docs, open the IDE, sync the event model
- Outputs: exact
neo commands, URLs, and port numbers
- Next: write-hurl-e2e (HTTP e2e tests that POST/GET these endpoints), wire-feature (wiring a new command or query so its endpoint exists), neo-cli (full CLI reference for build/test/lock), event-modeling (add features after
neo inspect sync)
1. Start the app
neo run
neo run --watch
The app binds to http://localhost:8080 by default. That default comes from Service.Transport.Web:
-- Service/Transport/Web.hs (neohaskell/neohaskell)
server :: WebTransport
server =
WebTransport
{ port = 8080,
...
}
The port is Config-derived, not hard-coded in the CLI. To override it, declare the env var in your Config.hs:
|> Config.field @Int "port" |> Config.doc "HTTP port" |> Config.envVar "PORT"
2. HTTP API surface
All routes are implemented in Service.Transport.Web.assembleTransport. This is the authoritative table:
| Method | Path | Notes |
|---|
POST | /commands/<kebab-name> | Execute a command; body is JSON |
GET | /queries/<kebab-name> | Read a query result |
GET | /openapi.json | OpenAPI 3.x spec (JSON) |
GET | /openapi.yaml | OpenAPI 3.x spec (YAML) |
GET | /docs | Scalar interactive docs UI |
GET | /health | Health check; 200 OK, always available |
GET | /ready | Readiness probe; enabled by default (200 = Ready, 503 = Rebuilding/Failed) |
Kebab-case conversion. The router converts the URL path segment to PascalCase before looking up the handler. The URL /commands/increment-counter maps to the IncrementCounter command type. The URL /queries/counter-value maps to the CounterValue query type.
Response shapes. A command returns {"entityId":"<uuid>"} (even creation commands); a query returns a paginated envelope {items, total, hasMore} and is eventually consistent — the projection lags the accepted command, so query assertions need retry (see write-hurl-e2e).
3. Copy-paste template
Replace increment-counter / counter-value with your actual kebab-case names.
curl
curl -s -X POST http://localhost:8080/commands/increment-counter \
-H "Content-Type: application/json" \
-d '{"entityId":"00000000-0000-0000-0000-000000000001","amount":1}'
curl -s http://localhost:8080/queries/counter-value
curl -s http://localhost:8080/openapi.json | jq .
curl -s http://localhost:8080/openapi.yaml
open http://localhost:8080/docs
xdg-open http://localhost:8080/docs
curl -s http://localhost:8080/health
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/ready
Hurl (for use with neo test or write-hurl-e2e)
# tests/counter/increment.hurl
POST http://localhost:8080/commands/increment-counter
Content-Type: application/json
{
"entityId": "00000000-0000-0000-0000-000000000001",
"amount": 1
}
HTTP 200
GET http://localhost:8080/queries/counter-value
HTTP 200
4. /ready is enabled by default
Application.new sets readinessConfig = Just ReadinessConfig { readinessPath = "ready", includeQueryStatus = True }, so the endpoint is live from the first neo run without any extra wiring:
- 200 — app is Ready
- 503 — app is Rebuilding or Failed
Application.useReadinessEndpoint :: Application -> Application re-enables the endpoint after an explicit disable; you only need to call it if something in your App.hs has set readinessConfig = Nothing. Under normal circumstances (starting from Application.new) you can hit /ready immediately and load-balancer readiness checks will work out of the box.
5. OpenAPI title and version
The spec title and version come from Application.withApiInfo. If that call is absent, defaults from Service.Application.Types.defaultApiInfo are used ("API" / "1.0.0"). To customise:
-- App.hs
Application.run
|> Application.withApiInfo (\_ -> ApiInfo { apiTitle = "My App", apiVersion = "0.1.0", apiDescription = "" })
...
6. The event-modeling IDE (neo ide)
neo ide
neo ide --port 9000
neo ide --host 0.0.0.0
The host argument must be an IP literal — localhost (a hostname) is rejected by the CLI parser.
Open http://127.0.0.1:2323 in a browser. This is the in-browser event-modeling IDE. It is completely separate from the app API at :8080.
7. neo inspect
Print the project's domain layout as structured JSON, or filter to one section:
neo inspect
neo inspect domains
neo inspect commands
neo inspect events
neo inspect queries
neo inspect integrations
neo inspect wiring
neo inspect sync — CLOBBER WARNING
neo inspect sync
This rewrites event-model.json entirely from source code. Any hand-authored additions (made by the event-modeling skill) that are not yet implemented in Haskell source will be lost.
The safe direction: drive the model into code (implement → wire → sync), not the reverse.
- Run
neo inspect sync to bootstrap or refresh the model from existing code.
- Run it before editing
event-model.json by hand, not after.
- After adding a feature manually to
event-model.json, implement it first, then run sync if you want to verify round-trip fidelity.
8. DO / DON'T
| Wrong instinct | NeoHaskell-correct |
|---|
cabal run directly | neo run — Nix-wrapped, reconciles config first |
Invent neo openapi, neo serve, or neo start | No such subcommands; the running app serves its own OpenAPI at /openapi.json |
Navigate to /swagger or /swagger-ui | Navigate to /docs (Scalar UI); /swagger returns 404 |
Use port :2323 for API calls | :2323 is neo ide (event-modeling UI); the app API is :8080 |
Use port :8080 to open the IDE | :8080 is the app API; open http://127.0.0.1:2323 for the IDE |
Assume /ready must be manually wired | /ready is on by default via Application.new; use Application.useReadinessEndpoint only to re-enable after an explicit disable |
Run neo inspect sync after hand-editing event-model.json | Sync clobbers hand-authored edits — run it only to bootstrap or refresh from existing Haskell source |
| Hard-code port 8080 everywhere | Port is Config-derived (default 8080); to override, add a Config.envVar "PORT" field in Config.hs |
Pass "localhost" to neo ide --host | Must be an IP literal: 127.0.0.1 or 0.0.0.0; neo ide --host localhost is rejected by the CLI |
9. Verify
neo run &
sleep 2
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/health
curl -s http://localhost:8080/openapi.json | jq .info.title