Run Route Lens from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page scans the Dart - every call matched to its closing bracket, its named arguments read at their own depth - - rebuilds the go_router table with every full path, resolves every context.go, push and goNamed against it, and checks router wiring, named routes, pageless pushes, path slashes, parameters, the not-found page and nested flows, then hands it all to the run as facts. The run writes the review or the rewrite; the page then holds it to its own facts and re-scans a rewritten router with the same scanner (routereply.js).
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"route-lens"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, app, code, facts) is missing. The page always sends all five of task, app, code, carry, facts; carry is an empty string for review. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
Two lanes, chosen by task, in the order the work runs - inspect, then produce:
review- each of the page's findings confirmed (with the issue, the fix and a short code fix) or cleared (with why), up to five issues a scan cannot see on real lines, and the fixes in the order to make them, each naming its ids.rewrite- the whole rewritten routing file - MaterialApp.router, a clean route table, links that resolve, parameters read, a not-found page, nested flows that hold - with what changed and what was kept.
A missing or unknown task is answered as review. Both lanes send the same five fields. The facts are the page's scan, one finding per line with its id, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the review's decisions into carry for the rewrite lane. The review sends at most 24,000 characters of code (the middle of a longer file is cut on whole lines and marked; every finding is still in the facts); the rewrite lane takes at most 16,000.
The input
| Field | Required | What |
|---|---|---|
task | yes | review or rewrite. If it is missing or neither, do review and set "task": "review". |
app | yes | the app's name, the platforms it targets, and the GoRouter variable the page found. |
code | yes | the routing code: the GoRouter or MaterialApp and the screens that navigate. A line starting "// [... N lines cut by the page" marks code the page scanned but did not send. |
carry | send it (empty for review) | rewrite task only: the review's decisions, one per line as id | confirmed or cleared or extra | where | fix or reason; may be empty. |
facts | yes | the page's scan: one finding per line as id | area | severity | line | where | evidence | source, the route table (full path | line | parameters | screen) and every navigation call with whether its target resolves. A line starting "must:" applies to your reply. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
From the source skill, @flutter/flutter-implementing-navigation-and-routing - the only standard the run applies:
- A route is a widget. Navigator (imperative push and pop) suits a small app with no deep linking; an app that needs deep links, web URLs or complex flows uses the Router through go_router.
- Avoid named routes (MaterialApp.routes, Navigator.pushNamed) for app navigation: their deep linking is rigid and the browser forward button does not work.
- With go_router: MaterialApp.router(routerConfig: the GoRouter), one GoRouter that defines every top-level route, and context.go('/path') or context.push('/path') instead of Navigator.push. Declarative routes are page-backed and deep-linkable; an imperative push (a MaterialPageRoute, a dialog, a bottom sheet) is pageless, has no URL, and is removed when the page-backed route under it is removed.
- go_router paths: a top-level path is absolute ('/cart'); a sub-route's path is relative ('details', not '/details'); a :parameter in a path is read in the builder from state.pathParameters and passed to the screen's constructor.
- Pass data to a screen through its constructor; return data with Navigator.pop(context, result) and await the push.
- A nested flow (a multi-step setup, persistent tabs) has its own Navigator with a key - a GlobalKey<NavigatorState> kept in the host's State - an onGenerateRoute that maps step names to pages and throws on an unknown name, and PopScope around the host so the back button does not pop the whole flow. Steps advance with navigatorKey.currentState!.pushNamed(step).
House rules
- Use only the input. Never invent a screen, a path, a parameter or a string the code does not hold. When a fix needs a new path or screen, say so and name it from what the code already shows (a /cart route for a CartScreen).
- The page's ids, lines and route table are measured facts: never renumber an id.
- No web links (http or https) in prose.
- Write in English. Keep the JSON keys and the ids exactly as given.
- Never use backticks. Name classes and calls plainly (GoRouter, context.go). Words from the rules above are free to use; what you never do is paste a whole sentence from this prompt.
- Never write a gendered pronoun for a user; write they, the user or the person.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
review
Confirm or clear each of the page's findings, add what a scan cannot see, and put the fixes in order.
summary- one sentence, at most 40 words: what a user of this app meets today (a crash on start, a tap that goes nowhere, a deep link that opens the wrong page) and the first fix.findings- one object per page finding you confirm, in id order:id(the page's id),widget(where it is, e.g. "GoRoute - the product page"),issue(one sentence: what goes wrong for the person using the app),fix(one sentence: the change),code(the corrected Dart for that piece only, at most 6 lines; "" when fix says it all).cleared- one object per page finding that is not a problem in this code, withidandwhy(one sentence tied to the code - e.g. a pageless push of a dialog is meant to have no URL). Every page id appears exactly once, in findings or in cleared.extra- at most 5 issues the scan cannot see, each tied to a line of the code:id(X1, X2, ...),area(wiring, routes, deeplinks, named or nested),line(a number),widget,issue,fix. Examples: data passed through route arguments instead of a constructor, a pushed screen whose result is never awaited, a redirect that loops, a tab bar that loses its stack, a go where a push would keep the back button.[]when there are none.plan- 2 to 10 short strings, the fixes in the order to make them (what crashes or leaves the router unused first, then the route table, then links and deep links, then nested flows), each naming the ids it covers (e.g. "Wire the GoRouter in with MaterialApp.router (R5)"). Every confirmed id appears in a step.[]only when nothing is confirmed.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "review",
"summary": "one sentence",
"findings": [ { "id": "R1", "widget": "MaterialApp - the app root", "issue": "one sentence", "fix": "one sentence", "code": "MaterialApp.router(routerConfig: router)" } ],
"cleared": [ { "id": "R4", "why": "one sentence" } ],
"extra": [ { "id": "X1", "area": "deeplinks", "line": 20, "widget": "GoRoute - the cart", "issue": "one sentence", "fix": "one sentence" } ],
"plan": [ "Wire the GoRouter in with MaterialApp.router (R5)" ],
"caveats": [ ]
}
The page holds it to its own facts (check() in routereply.js; any warning makes the reply flagged): the reply answers the task it was sent; every page finding id appears exactly once across findings and cleared, and no id the page never found; a cleared id has a reason; a confirmed id has an issue and a fix; at most five extra issues, each on a line inside the paste; a fix order of 2 to 10 steps that names every confirmed id; a summary; no web link in the prose.
rewrite
Rewrite the file with the fixes applied.
summary- one sentence, at most 40 words: what the rewrite changes and what it leaves for the team.code- the complete rewritten file. Keep every import, class, constructor parameter, screen, callback and behaviour; change only what the findings name, plus the import a change needs (package:go_router/go_router.dart). Never shorten the file with "..." or "unchanged" - write every line. If the code holds a line starting "// [... N lines cut by the page", keep that line exactly where it is. A GoRouter the app never uses - MaterialApp.router(routerConfig: the router), keeping the app's other arguments (title, theme); drop home: and routes:. Named routes - a GoRouter with one GoRoute per named route (the same paths), MaterialApp.router, and context.go or context.push for each Navigator.pushNamed. Route arguments become a path parameter or the constructor argument the screen takes. A Navigator.push in a go_router app - a GoRoute for that screen (under its parent, relative path) and context.push('/path'). A sub-route path with '/', or a top-level path without it - fix the slash only. A duplicate path - keep the first. An unread :parameter - read state.pathParameters['name']! in the builder and pass it to the screen; when the screen has no such constructor parameter yet, add one and say so in changes. An initialLocation or a link that matches no route - point it at the declared path it means (the route table in the facts), or add the route when the screen exists. No not-found page - errorBuilder: (context, state) => a small Scaffold that says the page was not found, with a way home. A nested flow: key: a final GlobalKey<NavigatorState> in the State class; onGenerateRoute with default: throw StateError(...); PopScope(canPop: false, onPopInvokedWithResult: ...) instead of WillPopScope, keeping what the old callback did.changes- one object per page finding you fixed, in id order:id,change(one sentence: what changed, naming the widget or path). Extra ids (X1, ...) from the carried review may appear too.kept- one object per page finding you did not change:id,why(one sentence - the review cleared it, or it needs a decision the code cannot make). Every page id appears exactly once, in changes or in kept; an id the carried review cleared belongs in kept.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "rewrite",
"summary": "one sentence",
"code": "the complete rewritten Dart file",
"changes": [ { "id": "R1", "change": "one sentence" } ],
"kept": [ { "id": "R4", "why": "one sentence" } ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; every page finding id appears exactly once across changes and kept; every widget class of the paste survives; the file is not shortened (at least 70% of its lines) and elides nothing; and the rewritten code is scanned again - a rule the changes claim fixed that still fires, or a rule the paste never had, is flagged. A declared path the rewrite drops is noted.
Worked examples
Three runs from the page's free samples (invented apps): the review of a shop whose router is never wired in, the rewrite of the same file carrying that review, and the review of a web guide still on named routes. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the code, the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.
review - A shop whose router is never wired in
The page read it as: Routing is broken - 2 blocker, 4 major, 1 minor; findings R1 no-error-page, R2 initial-unmatched, R3 subroute-slash, R4 param-unused, R5 router-unwired, R6 dead-link, R7 pageless-push.
Input (facts is 2245 characters; the first 1390 are shown):
{
"task": "review",
"app": "App: Larkspan Grocer\nTargets: mobile and the web\nRouter variable: router",
"code": "import 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nfinal router = GoRouter(\n initialLocation: '/home',\n routes: [\n GoRoute(\n path: '/',\n builder: (context, state) => const CatalogScreen(),\n routes: [\n GoRoute(\n path: '/product/:id',\n builder: (context, state) => const ProductScreen(),\n ),\n GoRoute(\n path: 'cart',\n builder: (context, state) => const CartScreen(),\n ),\n ],\n ),\n ],\n);\n\nclass ShopApp extends StatelessWidget {\n const ShopApp({super.key});\n\n @override\n Widget build(BuildContext context) {\n return MaterialApp(title: 'Larkspan Grocer', home: const CatalogScreen());\n }\n}\n\nclass CatalogScreen extends StatelessWidget {\n const CatalogScreen({super.key});\n\n @override\n Widget build(BuildContext context) {\n return Scaffold(\n appBar: AppBar(actions: [\n IconButton(tooltip: 'Cart', icon: const Icon(Icons.shopping_cart), onPressed: () => context.go('/basket')),\n ]),\n body: ListTile(\n title: const Text('Trail runner'),\n onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ProductScreen())),\n ),\n );\n }\n}\n",
"carry": "",
"facts": "The page's scan of 49 lines (20 calls); verdict: Routing is broken - 2 blocker, 4 major, 1 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- R1 | deeplinks | minor | line 4 | GoRouter | GoRouter has no errorBuilder, so a mistyped or stale deep link lands on go_router's default error screen. | source: final router = GoRouter(\n- R2 | deeplinks | major | line 5 | GoRouter | initialLocation '/home' matches no route in the table, so the app opens on the error screen. | source: initialLocation: '/home',\n- R3 | routes | blocker | line 12 | GoRoute | The sub-route path '/product/:id' starts with '/' - go_router asserts that a nested route's path is relative, so the app throws when the router is built. | source: path: '/product/:id',\n- R4 | routes | major | line 12 | GoRoute | The path '/product/:id' declares :id but its builder never reads state.pathParameters['id'], so every id opens the same page. | source: path: '/product/:id',\n- R5 | wiring | blocker | line 29 | MaterialApp | A GoRouter is defined but the app is a plain MaterialApp(...), so the router - and every deep link - is never used. | source: return MaterialApp(title: 'Larkspan Grocer', home: const CatalogScreen());\n- R6 | deeplinks | major | line 40 | - | context.go('/basket') matches no route in the table - the tap lands on the error screen. | source: IconButton(tooltip: 'Cart', icon: const\n\n[... 855 more characters cut from this display ...]"
}
Reply:
{
"task": "review",
"summary": "Today the app throws while the GoRouter is built because the product sub-route path starts with a slash, and even then the router is never wired in; first make the product path relative and switch to MaterialApp.router.",
"findings": [
{
"id": "R1",
"widget": "GoRouter - the router",
"issue": "A person who opens a mistyped or stale link sees go_router's bare default error screen with no way back to the catalog.",
"fix": "Add an errorBuilder that returns a small Scaffold saying the page was not found, with a button that calls context.go('/').",
"code": "errorBuilder: (context, state) => Scaffold(\n body: Center(child: TextButton(\n onPressed: () => context.go('/'),\n child: const Text('Page not found - back to the shop'),\n )),\n),"
},
{
"id": "R2",
"widget": "GoRouter - initialLocation",
"issue": "The app starts at '/home', which no route declares, so every launch opens on the error screen instead of the catalog.",
"fix": "Point initialLocation at the declared catalog path '/', or drop it since '/' is the default.",
"code": "initialLocation: '/',"
},
{
"id": "R3",
"widget": "GoRoute - the product page",
"issue": "The nested product route uses the absolute path '/product/:id', so go_router's assertion fails when the router is built and the app crashes on start.",
"fix": "Make the sub-route path relative so its full path stays /product/:id under the catalog route.",
"code": "path: 'product/:id',"
},
{
"id": "R4",
"widget": "GoRoute - the product page",
"issue": "The builder ignores :id, so a link to any product opens the same ProductScreen with no idea which product was asked for.",
"fix": "Read state.pathParameters['id']! in the builder and pass it to ProductScreen, adding an id constructor parameter to ProductScreen since it takes none today.",
"code": "builder: (context, state) =>\n ProductScreen(id: state.pathParameters['id']!),"
},
{
"id": "R5",
"widget": "MaterialApp - the app root",
"issue": "ShopApp builds a plain MaterialApp with home: CatalogScreen, so the GoRouter is never used, web URLs and deep links do nothing, and every context.go call fails because no GoRouter is above it.",
"fix": "Return MaterialApp.router with routerConfig: router, keeping the title and dropping home.",
"code": "return MaterialApp.router(\n title: 'Larkspan Grocer',\n routerConfig: router,\n);"
},
{
"id": "R6",
"widget": "IconButton - the Cart action on CatalogScreen",
"issue": "Tapping the cart icon goes to '/basket', which no route declares, so the person lands on the error screen instead of the cart.",
"fix": "Point the tap at the declared cart path '/cart'.",
"code": "onPressed: () => context.go('/cart'),"
},
{
"id": "R7",
"widget": "ListTile - the Trail runner row on CatalogScreen",
"issue": "The tap pushes ProductScreen as a pageless MaterialPageRoute, so the product page has no URL, cannot be shared or reloaded on the web, and bypasses the declared /product/:id route.",
"fix": "Replace Navigator.push with context.push to the product path, using the product's id once the catalog holds one, since the code shows only the title 'Trail runner'.",
"code": "onTap: () => context.push('/product/$id'),"
}
],
"cleared": [],
"extra": [],
"plan": [
"Make the product sub-route path relative so the router builds without asserting (R3)",
"Wire the GoRouter in with MaterialApp.router and drop home (R5)",
"Point initialLocation at the catalog path '/' (R2)",
"Read the :id path parameter and pass it to a new ProductScreen id constructor parameter (R4)",
"Change the cart icon target from '/basket' to '/cart' (R6)",
"Replace the Navigator.push of ProductScreen with context.push to its product path (R7)",
"Add an errorBuilder with a way back to the catalog (R1)"
],
"caveats": [
"ProductScreen and CartScreen are not defined in the pasted code, so the new id parameter on ProductScreen is a change the team makes in that class.",
"The catalog row has no product id in the code; the push target needs one from the team's product data."
]
}
What step 5 prints for it: R1: Add an errorBuilder that returns a small Scaffold saying the page was not found, with a button that calls context.go('/'). | R2: Point initialLocation at the declared catalog path '/', or drop it since '/' is the default. | R3: Make the sub-route path relative so its full path stays /product/:id under the catalog route. | R4: Read state.pathParameters['id']! in the builder and pass it to ProductScreen, adding an id constructor parameter to ProductScreen since it takes none today. | R5: Return MaterialApp.router with routerConfig: router, keeping the title and dropping home. | R6: Point the tap at the declared cart path '/cart'. | R7: Replace Navigator.push with context.push to the product path, using the product's id once the catalog holds one, since the code shows only the title 'Trail runner'.
The page's check: kept - nothing to flag
rewrite - A shop whose router is never wired in
The input carries the saved review (R.laneForm fills carry from the review reply), as the page's handoff does. The page re-scanned the rewritten code: 7 findings before, 0 after.
Input (facts is 2245 characters; the first 1390 are shown):
{
"task": "rewrite",
"app": "App: Larkspan Grocer\nTargets: mobile and the web\nRouter variable: router",
"code": "import 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nfinal router = GoRouter(\n initialLocation: '/home',\n routes: [\n GoRoute(\n path: '/',\n builder: (context, state) => const CatalogScreen(),\n routes: [\n GoRoute(\n path: '/product/:id',\n builder: (context, state) => const ProductScreen(),\n ),\n GoRoute(\n path: 'cart',\n builder: (context, state) => const CartScreen(),\n ),\n ],\n ),\n ],\n);\n\nclass ShopApp extends StatelessWidget {\n const ShopApp({super.key});\n\n @override\n Widget build(BuildContext context) {\n return MaterialApp(title: 'Larkspan Grocer', home: const CatalogScreen());\n }\n}\n\nclass CatalogScreen extends StatelessWidget {\n const CatalogScreen({super.key});\n\n @override\n Widget build(BuildContext context) {\n return Scaffold(\n appBar: AppBar(actions: [\n IconButton(tooltip: 'Cart', icon: const Icon(Icons.shopping_cart), onPressed: () => context.go('/basket')),\n ]),\n body: ListTile(\n title: const Text('Trail runner'),\n onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ProductScreen())),\n ),\n );\n }\n}\n",
"carry": "R1 | confirmed | GoRouter - the router | Add an errorBuilder that returns a small Scaffold saying the page was not found, with a button that calls context.go('/').\nR2 | confirmed | GoRouter - initialLocation | Point initialLocation at the declared catalog path '/', or drop it since '/' is the default.\nR3 | confirmed | GoRoute - the product page | Make the sub-route path relative so its full path stays /product/:id under the catalog route.\nR4 | confirmed | GoRoute - the product page | Read state.pathParameters['id']! in the builder and pass it to ProductScreen, adding an id constructor parameter to ProductScreen since it takes none today.\nR5 | confirmed | MaterialApp - the app root | Return MaterialApp.router with routerConfig: router, keeping the title and dropping home.\nR6 | confirmed | IconButton - the Cart action on CatalogScreen | Point the tap at the declared cart path '/cart'.\nR7 | confirmed | ListTile - the Trail runner row on CatalogScreen | Replace Navigator.push with context.push to the product path, using the product's id once the catalog holds one, since the code shows only the title 'Trail runner'.",
"facts": "The page's scan of 49 lines (20 calls); verdict: Routing is broken - 2 blocker, 4 major, 1 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- R1 | deeplinks | minor | line 4 | GoRouter | GoRouter has no errorBuilder, so a mistyped or stale deep link lands on go_router's default error screen. | source: final router = GoRouter(\n- R2 | deeplinks | major | line 5 | GoRouter | initialLocation '/home' matches no route in the table, so the app opens on the error screen. | source: initialLocation: '/home',\n- R3 | routes | blocker | line 12 | GoRoute | The sub-route path '/product/:id' starts with '/' - go_router asserts that a nested route's path is relative, so the app throws when the router is built. | source: path: '/product/:id',\n- R4 | routes | major | line 12 | GoRoute | The path '/product/:id' declares :id but its builder never reads state.pathParameters['id'], so every id opens the same page. | source: path: '/product/:id',\n- R5 | wiring | blocker | line 29 | MaterialApp | A GoRouter is defined but the app is a plain MaterialApp(...), so the router - and every deep link - is never used. | source: return MaterialApp(title: 'Larkspan Grocer', home: const CatalogScreen());\n- R6 | deeplinks | major | line 40 | - | context.go('/basket') matches no route in the table - the tap lands on the error screen. | source: IconButton(tooltip: 'Cart', icon: const\n\n[... 855 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "rewrite",
"summary": "Wires the GoRouter in with MaterialApp.router, fixes the start location, the product sub-route and the cart link, reads the product id and adds a not-found page; ProductScreen still needs an id parameter and the catalog a real product id.",
"code": "import 'package:flutter/material.dart';\nimport 'package:go_router/go_router.dart';\n\nfinal router = GoRouter(\n initialLocation: '/',\n errorBuilder: (context, state) => Scaffold(\n appBar: AppBar(title: const Text('Page not found')),\n body: Center(\n child: Column(\n mainAxisSize: MainAxisSize.min, [... 1321 more characters cut from this display]",
"changes": [
{
"id": "R1",
"change": "The GoRouter now has an errorBuilder that shows a small Scaffold saying the page was not found, with a button that calls context.go('/')."
},
{
"id": "R2",
"change": "The GoRouter initialLocation now points at the declared catalog path '/' instead of '/home'."
},
{
"id": "R3",
"change": "The product sub-route path is now the relative 'product/:id', so its full path stays /product/:id under the catalog route."
},
{
"id": "R4",
"change": "The product GoRoute builder reads state.pathParameters['id']! and passes it to ProductScreen as id, which needs a new required id constructor parameter on ProductScreen since it takes none today."
},
{
"id": "R5",
"change": "ShopApp now returns MaterialApp.router with routerConfig: router, keeping the title 'Larkspan Grocer' and dropping home."
},
{
"id": "R6",
"change": "The Cart IconButton on CatalogScreen now calls context.go('/cart'), the declared cart path."
},
{
"id": "R7",
"change": "The Trail runner ListTile on CatalogScreen now calls context.push to the /product/:id path instead of Navigator.push with a MaterialPageRoute, using the encoded title as the id until the catalog holds a real one."
}
],
"kept": [],
"caveats": [
"ProductScreen is not defined in this file, so the team must add a required String id constructor parameter to it.",
"The catalog shows only the title 'Trail runner', so the product link uses that title as its id until the catalog holds a real product id.",
"CartScreen is referenced but not defined in this file; it is left as it was."
]
}
Cut from this display only (the saved reply and the check use the full text): code (1635 characters, 314 shown).
What step 5 prints for it: R1: The GoRouter now has an errorBuilder that shows a small Scaffold saying the page was not found, with a button that calls context.go('/'). | R2: The GoRouter initialLocation now points at the declared catalog path '/' instead of '/home'. | R3: The product sub-route path is now the relative 'product/:id', so its full path stays /product/:id under the catalog route. | R4: The product GoRoute builder reads state.pathParameters['id']! and passes it to ProductScreen as id, which needs a new required id constructor parameter on ProductScreen since it takes none today. | R5: ShopApp now returns MaterialApp.router with routerConfig: router, keeping the title 'Larkspan Grocer' and dropping home. | R6: The Cart IconButton on CatalogScreen now calls context.go('/cart'), the declared cart path. | R7: The Trail runner ListTile on CatalogScreen now calls context.push to the /product/:id path instead of Navigator.push with a MaterialPageRoute, using the encoded title as the id until the catalog holds a real one.
The page's check: kept - nothing to flag
review - A web guide still on named routes
The page read it as: Fix before release - 0 blocker, 3 major, 0 minor; findings R1 named-routes, R2 named-push, R3 named-push.
Input:
{
"task": "review",
"app": "App: Thornelow Field Notes\nTargets: the web\nRouter variable: none found",
"code": "import 'package:flutter/material.dart';\n\nclass DocsApp extends StatelessWidget {\n const DocsApp({super.key});\n\n @override\n Widget build(BuildContext context) {\n return MaterialApp(\n title: 'Thornelow Field Notes',\n initialRoute: '/',\n routes: {\n '/': (context) => const GuideHome(),\n '/chapter': (context) => const ChapterScreen(),\n '/settings': (context) => const SettingsScreen(),\n },\n );\n }\n}\n\nclass GuideHome extends StatelessWidget {\n const GuideHome({super.key});\n\n @override\n Widget build(BuildContext context) {\n return Scaffold(\n body: ListView(\n children: [\n ListTile(\n title: const Text('Chapter 3 - Mosses'),\n onTap: () => Navigator.pushNamed(context, '/chapter', arguments: 3),\n ),\n ListTile(\n title: const Text('Settings'),\n onTap: () => Navigator.pushNamed(context, '/settings'),\n ),\n ],\n ),\n );\n }\n}\n",
"carry": "",
"facts": "The page's scan of 41 lines (14 calls); verdict: Fix before release - 0 blocker, 3 major, 0 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- R1 | named | major | line 11 | MaterialApp | MaterialApp declares routes: (named routes) - rigid deep linking, and no browser forward button on the web build. | source: routes: {\n- R2 | named | major | line 30 | - | Navigator.pushNamed navigates by a named route - named routes have rigid deep linking and do not support the browser forward button. | source: onTap: () => Navigator.pushNamed(context, '/chapter', arguments: 3),\n- R3 | named | major | line 34 | - | Navigator.pushNamed navigates by a named route - named routes have rigid deep linking and do not support the browser forward button. | source: onTap: () => Navigator.pushNamed(context, '/settings'),\nThe route table (full path | line | parameters | screen):\n- none (no GoRouter table)\nNavigation calls (line | call | target | status):\n- none\nPresent: 0 GoRouters, 0 routes, 0 nested Navigators; the app is not MaterialApp.router.\nmust: account for every finding id above exactly once - in findings (confirmed) or in cleared (with the reason)."
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "review",
"summary": "On the web build a reader who opens Chapter 3 gets a /chapter address that forgets the chapter number and a forward button that does nothing; first replace the routes: map with a GoRouter wired through MaterialApp.router.",
"findings": [
{
"id": "R1",
"widget": "MaterialApp - the app root in DocsApp",
"issue": "The routes: map with initialRoute makes every screen a named route, so a reader on the web cannot rely on the browser forward button and a shared link to a chapter cannot say which chapter it is.",
"fix": "Declare a top-level GoRouter with a '/' route for GuideHome and relative sub-routes 'chapter/:number' and 'settings', then build the app with MaterialApp.router(routerConfig: router), keeping title and dropping initialRoute and routes.",
"code": "final router = GoRouter(routes: [\n GoRoute(path: '/', builder: (context, state) => const GuideHome(), routes: [\n GoRoute(path: 'chapter/:number', builder: (context, state) => ChapterScreen(number: int.parse(state.pathParameters['number']!))),\n GoRoute(path: 'settings', builder: (context, state) => const [... 29 more characters cut from this display]"
},
{
"id": "R2",
"widget": "ListTile - Chapter 3 - Mosses on GuideHome",
"issue": "Tapping the chapter tile pushes the named route '/chapter' with arguments: 3, so the number 3 lives only in the route settings, never reaches the address bar, and is lost when the page is reloaded or the link is shared.",
"fix": "Put the chapter number in the path and navigate with context.go('/chapter/3'), which keeps GuideHome underneath as the parent route so the back button still returns to the list.",
"code": "onTap: () => context.go('/chapter/3'),"
},
{
"id": "R3",
"widget": "ListTile - Settings on GuideHome",
"issue": "Tapping Settings pushes the named route '/settings', which on the web gives the person a page the browser forward button cannot return to after they go back.",
"fix": "Navigate with context.go('/settings') to the settings sub-route declared under '/'.",
"code": "onTap: () => context.go('/settings'),"
}
],
"cleared": [],
"extra": [
{
"id": "X1",
"area": "routes",
"line": 13,
"widget": "ChapterScreen - the chapter page",
"issue": "ChapterScreen is built with an empty const constructor, so the chapter number 3 can only reach it through route arguments read from the route settings, and a direct visit to /chapter opens a chapter page with no chapter.",
"fix": "Give ChapterScreen a required int number constructor parameter (a new parameter the code does not yet have) and pass it from state.pathParameters['number'] in the sub-route builder instead of reading route arguments."
},
{
"id": "X2",
"area": "deeplinks",
"line": 8,
"widget": "MaterialApp - the app root in DocsApp",
"issue": "Nothing handles an address that matches no route, so a mistyped or outdated web link such as /chapter/abc leaves the person on an error screen with no way back to the guide.",
"fix": "Add an errorBuilder to the GoRouter that shows a small Scaffold saying the page was not found, with a button that calls context.go('/')."
}
],
"plan": [
"Add package:go_router/go_router.dart and replace the routes: map and initialRoute with a GoRouter passed to MaterialApp.router (R1)",
"Declare the chapter sub-route as 'chapter/:number' and give ChapterScreen a number constructor parameter filled from state.pathParameters (R1, X1)",
"Switch the chapter tile to context.go('/chapter/3') (R2)",
"Switch the settings tile to context.go('/settings') (R3)",
"Add an errorBuilder with a not-found page that links back to '/' (X2)"
],
"caveats": [
"ChapterScreen and SettingsScreen are not in the pasted code, so how ChapterScreen reads arguments today is not visible; check it before removing any lookup of route arguments.",
"int.parse throws on a non-numeric chapter segment; parse with int.tryParse and fall back to the not-found page if the team expects hand-typed links."
]
}
Cut from this display only (the saved reply and the check use the full text): findings[0].code (340 characters, 311 shown).
What step 5 prints for it: R1: Declare a top-level GoRouter with a '/' route for GuideHome and relative sub-routes 'chapter/:number' and 'settings', then build the app with MaterialApp.router(routerConfig: router), keeping title and dropping initialRoute and routes. | R2: Put the chapter number in the path and navigate with context.go('/chapter/3'), which keeps GuideHome underneath as the parent route so the back button still returns to the list. | R3: Navigate with context.go('/settings') to the settings sub-route declared under '/'.
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./routekit.js"); require("./routereply.js")), then:
const K = RouteKit, R = RouteReply;
const f = K.form({ name: "Acme", targets: "all", carry: "",
code: "final router = GoRouter(routes: [GoRoute(path: '/', builder: (c, s) => const Home(), routes: [GoRoute(path: '/item/:id', builder: (c, s) => const Item())])]);" });
// f.findings, f.scan.table (full paths), f.scan.links (resolved targets), f.checklist, f.test (the deep-link smoke test)
// K.findingsCsv(f), K.routesCsv(f), K.checklistMd(f) - the free exports
const b = K.buildInput(f, "review"); // or "rewrite" (R.laneForm(form, "rewrite", review) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "review");
// R.check(reply, b) // {status: kept or flags, notes}
Get a token for the steps below from /tokens.html.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN" # from https://route-lens.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://route-lens.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// route-lens.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://route-lens.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://route-lens.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// Route Lens.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class Route Lens {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://route-lens.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://route-lens.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://route-lens.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://route-lens.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"route-lens"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "route-lens"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "route-lens" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "route-lens"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "route-lens"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "route-lens" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "route-lens"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "route-lens" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "review", "app": "App: Larkspan Grocer
Targets: mobile and the web
Router variable: router", "code": "import 'package:go_router/go_router.dart';
...", "carry": "", "facts": "The page's scan of 49 lines ...
Findings (id | area | severity | line | where | evidence | source):
- R1 | deeplinks | minor | line 4 | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "review", "app": "App: Larkspan Grocer
Targets: mobile and the web
Router variable: router", "code": "import 'package:go_router/go_router.dart';
...", "carry": "", "facts": "The page's scan of 49 lines ...
Findings (id | area | severity | line | where | evidence | source):
- R1 | deeplinks | minor | line 4 | ..."}
# - see the input table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "review" or "rewrite", app, code, carry, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses route-lens:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A review reply carries summary, findings, cleared, extra, plan and caveats; a rewrite reply carries summary, code, changes, kept and caveats. The code below prints the gist of either: each confirmed finding and its fix, or each change.
KEY="route-lens:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "rewrite" then (.changes[] | "\(.id): \(.change)")
else (.findings[] | "\(.id): \(.fix)") end' reply.json
import hashlib, time
key = "route-lens:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "rewrite":
for x in reply["changes"]:
print(x["id"], x["change"])
else:
for x in reply["findings"]:
print(x["id"], x["fix"])
import { createHash } from "node:crypto";
const key = `route-lens:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.task);
if (reply.task === "rewrite") { for (const x of reply.changes) console.log(x.id, x.change); }
else { for (const x of reply.findings) console.log(x.id, x.fix); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "route-lens:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
key, b := "findings", "fix"
if reply["task"] == "rewrite" {
key, b = "changes", "change"
}
xs, _ := reply[key].([]any)
for _, x := range xs {
xx := x.(map[string]any)
fmt.Println(xx["id"], xx[b])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "route-lens:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("task").asText());
if (reply.path("task").asText().equals("rewrite")) {
for (JsonNode x : reply.path("changes")) System.out.println(x.path("id").asText() + " " + x.path("change").asText());
} else {
for (JsonNode x : reply.path("findings")) System.out.println(x.path("id").asText() + " " + x.path("fix").asText());
}
require "digest"
key = "route-lens:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
if reply["task"] == "rewrite"
reply["changes"].each { |x| puts "#{x["id"]} #{x["change"]}" }
else
reply["findings"].each { |x| puts "#{x["id"]} #{x["fix"]}" }
end
$key = "route-lens:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
if ($reply["task"] === "rewrite") { foreach ($reply["changes"] as $x) echo $x["id"], " ", $x["change"], "\n"; }
else { foreach ($reply["findings"] as $x) echo $x["id"], " ", $x["fix"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"route-lens:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "rewrite") { foreach (var x in reply["changes"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["change"]}"); }
else { foreach (var x in reply["findings"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["fix"]}"); }
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: route-lens:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "review", "app": "App: Larkspan Grocer
Targets: mobile and the web
Router variable: router", "code": "import 'package:go_router/go_router.dart';
...", "carry": "", "facts": "The page's scan of 49 lines ...
Findings (id | area | severity | line | where | evidence | source):
- R1 | deeplinks | minor | line 4 | ..."}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "route-lens:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `route-lens:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "route-lens:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "route-lens:" + input.get("task") + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "route-lens:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: route-lens:" . json_decode($rev, true)["task"] . ":" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"route-lens:{input["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);