Run Widget Bridge from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page splits the paste at its file lines, masks comments and strings in each file, resolves string constants, and lines the sides up - the App Group, the widget names updateWidget asks for, every key written and read with its type, the view ids and the manifest receiver, 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 the rewritten files with the same scanner (bridgereply.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":"widget-bridge"}), 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, unit, code, facts) is missing. The page always sends all five of task, unit, 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- every pasted file corrected under its own file line - one App Group, updateWidget naming the real iOS kind and Android provider, every key read as written and with its type, a provider that reads HomeWidgetPlugin.getData and calls updateAppWidget - with what changed and what was kept, every file, class and struct name unchanged.
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". |
unit | yes | the widget's name, its platforms and the files pasted. |
code | yes | the files, one after another, each starting with a line "// file: path" (XML files: "<!-- file: path -->"). A line starting "// [... N lines cut by the page" marks code the page scanned but did not send. Line numbers count from the top of the whole paste. |
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 key map, one key per line as key | Dart writes | iOS reads | Android reads | status; and the names (App Group, iOS widget kind, Android provider, layout) on each side. 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-adding-home-screen-widgets - the only standard the run applies:
- Flutter side: call HomeWidget.setAppGroupId with the App Group at startup; save values with HomeWidget.saveWidgetData<T>('key', value); then call HomeWidget.updateWidget(iOSName: the iOS widget, androidName: the Android widget) so the OS refreshes it. Await these calls.
- iOS: a Widget Extension target, with the App Groups capability on both the Runner target and the extension and the same group ID as Dart. A TimelineEntry holds the data; the TimelineProvider's getSnapshot and getTimeline read it with UserDefaults(suiteName: the App Group) and its string(forKey:) style getters; a SwiftUI View shows the entry.
- Android: an AppWidgetProvider whose onUpdate gets the data with HomeWidgetPlugin.getData(context), reads values with getString("key", default) style getters, sets them on RemoteViews over the widget's XML layout (setTextViewText, setImageViewBitmap), and calls appWidgetManager.updateAppWidget(appWidgetId, views). The provider is registered in the manifest.
- A complex UI can be rendered to an image with HomeWidget.renderFlutterWidget; the native side reads the file path from the shared storage and loads it (UIImage(contentsOfFile:) on iOS, BitmapFactory.decodeFile on Android).
- The page also applies what the home_widget package does: the names updateWidget passes must match the iOS widget's kind and the Android provider's class, a key the widget reads must be one the app writes with the same type, and on Android a Dart double is stored as its raw long bits.
House rules
- Use only the input. Never invent a key, a group ID, a class, a view id or a file the paste does not hold. When the sides disagree, the widget's own names win for widget names - iOSName becomes the Swift widget's kind, androidName the provider's class, since renaming a struct or class would break the rest - while for keys and the App Group the value the Flutter side writes wins unless the evidence says otherwise; say which you picked; when a fix needs a file that was not pasted (a layout, the manifest, the widget info XML), say so in a caveat instead of guessing its contents.
- The page's ids and lines 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 calls plainly (HomeWidget.updateWidget, appWidgetManager.updateAppWidget). 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 the widget shows today (often nothing, or stale or default values), and the first fix.findings- one object per page finding you confirm, in id order:id(the page's id),widget(where it sits - a file and class, e.g. "NewsWidget.kt - NewsWidget.onUpdate"),issue(one sentence: what the user sees on the home screen),fix(one sentence: the change),code(the corrected code for that piece only, at most 6 lines, in that file's language; "" 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). 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 paste:id(X1, X2, ...),area(names, keys, ios, android or dart),line(a number),widget,issue,fix. Examples: a key built at run time that may not match, a timeline policy that never refreshes, a placeholder that crashes or shows raw keys, an image path the native side never loads, a widget the Flutter side updates before the app group is set, a one-side-only key the other platform should also show.[]when there are none.plan- 2 to 10 short strings, the fixes in the order to make them (what keeps the widget blank or the build failing first, then names and the App Group, then keys and types, then the rest), each naming the ids it covers (e.g. "Use one App Group ID in Dart and Swift (B2)"). 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": "B1", "widget": "widget_sync.dart - pushHeadline", "issue": "one sentence", "fix": "one sentence", "code": "await HomeWidget.updateWidget(iOSName: 'NewsWidgets', androidName: 'NewsWidget');" } ],
"cleared": [ { "id": "B4", "why": "one sentence" } ],
"extra": [ { "id": "X1", "area": "ios", "line": 40, "widget": "NewsWidgets.swift - Provider.getTimeline", "issue": "one sentence", "fix": "one sentence" } ],
"plan": [ "Use one App Group ID in Dart and Swift (B2)" ],
"caveats": [ ]
}
The page holds it to its own facts (check() in bridgereply.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 files with the fixes applied.
summary- one sentence, at most 40 words: what the rewrite changes and what it leaves for the team.code- every pasted file, rewritten, in the same order, each under its own unchanged "// file:" (or "<!-- file: -->") line. Keep every import, class, struct, function and layout view, with every name unchanged; change only what the findings name, plus the imports a change needs. Never shorten a file with "..." or "unchanged" - write every line. Never add a file that was not pasted. If the code holds a line starting "// [... N lines cut by the page", keep that line exactly where it is. App Group missing or different - one ID, set with await HomeWidget.setAppGroupId before any save, the same string in UserDefaults(suiteName:). UserDefaults.standard - UserDefaults(suiteName: the App Group). updateWidget missing a name or naming the wrong one - iOSName is the Widget struct's kind string, androidName the provider's class name. A key read but never written, or spelt two ways - use the key the Flutter side writes. A type mismatch - read with the getter that matches the saved type (integer(forKey:), getInt; on Android a double saved from Dart is read with getLong and java.lang.Double.longBitsToDouble), or change the save to the type the widget shows. A force-unwrapped read - ?? with a placeholder. A provider that never calls updateAppWidget - call appWidgetManager.updateAppWidget(appWidgetId, views) inside the loop; its own SharedPreferences - HomeWidgetPlugin.getData(context). A view id the layout lacks - the id the layout defines (or add the id to the layout when the layout is pasted). A save not awaited - await it before updateWidget. A provider missing from a pasted manifest - a receiver with the APPWIDGET_UPDATE intent filter and the android.appwidget.provider meta-data.changes- one object per page finding you fixed, in id order:id,change(one sentence: what changed, naming the file). 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 file or a decision the paste does not hold). 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": "every file, rewritten, each under its own file line",
"changes": [ { "id": "B1", "change": "one sentence" } ],
"kept": [ { "id": "B4", "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 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 file line, class or struct the rewrite drops or renames is flagged.
Worked examples
Three runs from the page's free samples (invented apps): the review of a news widget whose Flutter and native sides never meet, the rewrite of the same files carrying that review, and the review of a step counter that writes one type and reads another. 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 news widget whose two sides never meet
The page read it as: Widget stays blank - 3 blocker, 2 major, 0 minor; findings B1 ios-name-mismatch, B2 group-mismatch, B3 no-update-call, B4 key-unwritten, B5 missing-view-id.
Input (code is 3193 characters; the first 1380 are shown; facts is 2493 characters; the first 1399 are shown):
{
"task": "review",
"unit": "Widget: News headline widget\nPlatforms: iOS and Android\nFiles: lib/news/widget_sync.dart (Dart), ios/NewsWidgets/NewsWidgets.swift (Swift), android/app/src/main/kotlin/com/example/news/NewsWidget.kt (Kotlin/Java), android/app/src/main/res/layout/news_widget.xml (Layout XML)",
"code": "// file: lib/news/widget_sync.dart\nimport 'package:home_widget/home_widget.dart';\n\nconst String appGroupId = 'group.com.example.news';\n\nFuture<void> pushHeadline(String title, String description) async {\n await HomeWidget.setAppGroupId(appGroupId);\n await HomeWidget.saveWidgetData<String>('headline_title', title);\n await HomeWidget.saveWidgetData<String>('headline_description', description);\n await HomeWidget.updateWidget(iOSName: 'NewsWidget', androidName: 'NewsWidget');\n}\n\n// file: ios/NewsWidgets/NewsWidgets.swift\nimport WidgetKit\nimport SwiftUI\n\nstruct NewsEntry: TimelineEntry {\n let date: Date\n let title: String\n let description: String\n}\n\nstruct Provider: TimelineProvider {\n func placeholder(in context: Context) -> NewsEntry {\n NewsEntry(date: Date(), title: \"Loading...\", description: \"\")\n }\n\n func getSnapshot(in context: Context, completion: @escaping (NewsEntry) -> ()) {\n let defaults = UserDefaults(suiteName: \"group.com.example.app\")\n let title = defaults?.string(forKey: \"headline_title\") ?? \"No title\"\n let description = defaults?.string(forKey: \"headline_description\") ?? \"\"\n completion(NewsEntry(date: Date(), title: title, description: description))\n }\n\n func getTimeline(in context: Context, completion: @escaping (Timeline<NewsEntry>) -> ()) {\n getSnapshot(in: context) { entry in\n\n[... 1813 more characters cut from this display ...]",
"carry": "",
"facts": "The page's scan of 79 lines in 4 files (lib/news/widget_sync.dart = Dart, lines 2-12; ios/NewsWidgets/NewsWidgets.swift = Swift, lines 14-51; android/app/src/main/kotlin/com/example/news/NewsWidget.kt = Kotlin/Java, lines 53-72; android/app/src/main/res/layout/news_widget.xml = Layout XML, lines 74-79); verdict: Widget stays blank - 3 blocker, 2 major, 0 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- B1 | names | major | line 10 | updateWidget | updateWidget asks iOS to reload 'NewsWidget', but the widget's kind is 'NewsWidgets' - WidgetCenter reloads by kind, so nothing refreshes. | source: await HomeWidget.updateWidget(iOSName: 'NewsWidget', androidName: 'NewsWidget');\n- B2 | names | blocker | line 29 | App Group | The App Group differs: Dart sets 'group.com.example.news' (line 7) but the widget reads UserDefaults(suiteName: \"group.com.example.app\") - the two sides never see the same storage. | source: let defaults = UserDefaults(suiteName: \"group.com.example.app\")\n- B3 | android | blocker | line 61 | NewsWidget | NewsWidget never calls appWidgetManager.updateAppWidget, so the RemoteViews it builds are never shown. | source: class NewsWidget : AppWidgetProvider() {\n- B4 | keys | major | line 67 | NewsWidget.kt | The Android widget reads 'headline_desc', which the Flutter side never saves (it writes 'headline_title', 'headline_description') - the\n\n[... 1094 more characters cut from this display ...]"
}
Reply:
{
"task": "review",
"summary": "Today the iOS widget shows No title because it reads a different App Group, and the Android widget never appears; first use group.com.example.news in Swift and call appWidgetManager.updateAppWidget.",
"findings": [
{
"id": "B1",
"widget": "widget_sync.dart - pushHeadline",
"issue": "On iOS the home screen widget is never asked to reload, so a new headline stays hidden until the system refreshes on its own.",
"fix": "Pass the widget's kind string NewsWidgets as iOSName and keep NewsWidget as androidName.",
"code": "await HomeWidget.updateWidget(iOSName: 'NewsWidgets', androidName: 'NewsWidget');"
},
{
"id": "B2",
"widget": "NewsWidgets.swift - Provider.getSnapshot",
"issue": "The iOS widget always shows No title and an empty description because it opens a storage suite the app never writes.",
"fix": "Use the App Group the Flutter side sets, group.com.example.news, in UserDefaults(suiteName:).",
"code": "let defaults = UserDefaults(suiteName: \"group.com.example.news\")"
},
{
"id": "B3",
"widget": "NewsWidget.kt - NewsWidget.onUpdate",
"issue": "The Android widget stays blank because the RemoteViews built in the loop are never handed to the widget manager.",
"fix": "Call appWidgetManager.updateAppWidget(appWidgetId, views) at the end of each loop pass.",
"code": "appWidgetManager.updateAppWidget(appWidgetId, views)"
},
{
"id": "B4",
"widget": "NewsWidget.kt - NewsWidget.onUpdate",
"issue": "The Android widget never shows the headline description because it reads headline_desc, a key the app never saves.",
"fix": "Read headline_description, the key the Flutter side writes.",
"code": "setTextViewText(R.id.headline_description, widgetData.getString(\"headline_description\", \"\"))"
},
{
"id": "B5",
"widget": "NewsWidget.kt - NewsWidget.onUpdate",
"issue": "The Android build fails because R.id.headline_body is not defined in news_widget.xml, so the widget never reaches the home screen.",
"fix": "Set the text on R.id.headline_description, the id the pasted layout defines.",
"code": "setTextViewText(R.id.headline_description, widgetData.getString(\"headline_description\", \"\"))"
}
],
"cleared": [],
"extra": [
{
"id": "X1",
"area": "ios",
"line": 36,
"widget": "NewsWidgets.swift - Provider.getTimeline",
"issue": "The single-entry timeline with the atEnd policy asks WidgetKit for a new timeline right away, spending the refresh budget on reads that return the same headline.",
"fix": "Use the never policy, since the app already asks for a reload through HomeWidget.updateWidget after each save."
}
],
"plan": [
"Fix the Android layout id so the build passes and call appWidgetManager.updateAppWidget in the loop (B5, B3)",
"Use the App Group group.com.example.news in Swift as well as Dart (B2)",
"Pass the iOS kind NewsWidgets as iOSName in HomeWidget.updateWidget (B1)",
"Read headline_description on Android, the key Dart writes (B4)",
"Switch the iOS timeline policy to never (X1)"
],
"caveats": [
"The App Groups capability with group.com.example.news must be on both the Runner target and the widget extension; the project settings were not pasted.",
"The AndroidManifest.xml was not pasted, so the NewsWidget receiver registration could not be checked."
]
}
What step 5 prints for it: B1: Pass the widget's kind string NewsWidgets as iOSName and keep NewsWidget as androidName. | B2: Use the App Group the Flutter side sets, group.com.example.news, in UserDefaults(suiteName:). | B3: Call appWidgetManager.updateAppWidget(appWidgetId, views) at the end of each loop pass. | B4: Read headline_description, the key the Flutter side writes. | B5: Set the text on R.id.headline_description, the id the pasted layout defines.
The page's check: kept - nothing to flag
rewrite - A news widget whose two sides never meet
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: 5 findings before, 0 after.
Input (code is 3193 characters; the first 1380 are shown; facts is 2485 characters; the first 1399 are shown):
{
"task": "rewrite",
"unit": "Widget: News headline widget\nPlatforms: iOS and Android\nFiles: lib/news/widget_sync.dart (Dart), ios/NewsWidgets/NewsWidgets.swift (Swift), android/app/src/main/kotlin/com/example/news/NewsWidget.kt (Kotlin/Java), android/app/src/main/res/layout/news_widget.xml (Layout XML)",
"code": "// file: lib/news/widget_sync.dart\nimport 'package:home_widget/home_widget.dart';\n\nconst String appGroupId = 'group.com.example.news';\n\nFuture<void> pushHeadline(String title, String description) async {\n await HomeWidget.setAppGroupId(appGroupId);\n await HomeWidget.saveWidgetData<String>('headline_title', title);\n await HomeWidget.saveWidgetData<String>('headline_description', description);\n await HomeWidget.updateWidget(iOSName: 'NewsWidget', androidName: 'NewsWidget');\n}\n\n// file: ios/NewsWidgets/NewsWidgets.swift\nimport WidgetKit\nimport SwiftUI\n\nstruct NewsEntry: TimelineEntry {\n let date: Date\n let title: String\n let description: String\n}\n\nstruct Provider: TimelineProvider {\n func placeholder(in context: Context) -> NewsEntry {\n NewsEntry(date: Date(), title: \"Loading...\", description: \"\")\n }\n\n func getSnapshot(in context: Context, completion: @escaping (NewsEntry) -> ()) {\n let defaults = UserDefaults(suiteName: \"group.com.example.app\")\n let title = defaults?.string(forKey: \"headline_title\") ?? \"No title\"\n let description = defaults?.string(forKey: \"headline_description\") ?? \"\"\n completion(NewsEntry(date: Date(), title: title, description: description))\n }\n\n func getTimeline(in context: Context, completion: @escaping (Timeline<NewsEntry>) -> ()) {\n getSnapshot(in: context) { entry in\n\n[... 1813 more characters cut from this display ...]",
"carry": "B1 | confirmed | widget_sync.dart - pushHeadline | Pass the widget's kind string NewsWidgets as iOSName and keep NewsWidget as androidName.\nB2 | confirmed | NewsWidgets.swift - Provider.getSnapshot | Use the App Group the Flutter side sets, group.com.example.news, in UserDefaults(suiteName:).\nB3 | confirmed | NewsWidget.kt - NewsWidget.onUpdate | Call appWidgetManager.updateAppWidget(appWidgetId, views) at the end of each loop pass.\nB4 | confirmed | NewsWidget.kt - NewsWidget.onUpdate | Read headline_description, the key the Flutter side writes.\nB5 | confirmed | NewsWidget.kt - NewsWidget.onUpdate | Set the text on R.id.headline_description, the id the pasted layout defines.\nX1 | extra | NewsWidgets.swift - Provider.getTimeline (line 36) | Use the never policy, since the app already asks for a reload through HomeWidget.updateWidget after each save.",
"facts": "The page's scan of 79 lines in 4 files (lib/news/widget_sync.dart = Dart, lines 2-12; ios/NewsWidgets/NewsWidgets.swift = Swift, lines 14-51; android/app/src/main/kotlin/com/example/news/NewsWidget.kt = Kotlin/Java, lines 53-72; android/app/src/main/res/layout/news_widget.xml = Layout XML, lines 74-79); verdict: Widget stays blank - 3 blocker, 2 major, 0 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- B1 | names | major | line 10 | updateWidget | updateWidget asks iOS to reload 'NewsWidget', but the widget's kind is 'NewsWidgets' - WidgetCenter reloads by kind, so nothing refreshes. | source: await HomeWidget.updateWidget(iOSName: 'NewsWidget', androidName: 'NewsWidget');\n- B2 | names | blocker | line 29 | App Group | The App Group differs: Dart sets 'group.com.example.news' (line 7) but the widget reads UserDefaults(suiteName: \"group.com.example.app\") - the two sides never see the same storage. | source: let defaults = UserDefaults(suiteName: \"group.com.example.app\")\n- B3 | android | blocker | line 61 | NewsWidget | NewsWidget never calls appWidgetManager.updateAppWidget, so the RemoteViews it builds are never shown. | source: class NewsWidget : AppWidgetProvider() {\n- B4 | keys | major | line 67 | NewsWidget.kt | The Android widget reads 'headline_desc', which the Flutter side never saves (it writes 'headline_title', 'headline_description') - the\n\n[... 1086 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "rewrite",
"summary": "The rewrite points iOSName at the NewsWidgets kind, aligns the App Group, fixes the Android key and view id, adds updateAppWidget and the never policy; the widget info XML and manifest stay for the team.",
"code": "// file: lib/news/widget_sync.dart\nimport 'package:home_widget/home_widget.dart';\n\nconst String appGroupId = 'group.com.example.news';\n\nFuture<void> pushHeadline(String title, String description) async {\n await HomeWidget.setAppGroupId(appGroupId);\n await HomeWidget.saveWidgetData<String>('headline_title', title); [... 2956 more characters cut from this display]",
"changes": [
{
"id": "B1",
"change": "widget_sync.dart now passes NewsWidgets, the Widget struct's kind string, as iOSName and keeps NewsWidget as androidName."
},
{
"id": "B2",
"change": "NewsWidgets.swift now reads UserDefaults(suiteName: \"group.com.example.news\"), the App Group the Flutter side sets."
},
{
"id": "B3",
"change": "NewsWidget.kt now calls appWidgetManager.updateAppWidget(appWidgetId, views) at the end of each loop pass."
},
{
"id": "B4",
"change": "NewsWidget.kt now reads headline_description, the key the Flutter side writes, instead of headline_desc."
},
{
"id": "B5",
"change": "NewsWidget.kt now sets the text on R.id.headline_description, the id news_widget.xml defines."
},
{
"id": "X1",
"change": "NewsWidgets.swift getTimeline now uses the never policy, since HomeWidget.updateWidget asks for a reload after each save."
}
],
"kept": [],
"caveats": [
"The App Groups capability with group.com.example.news must be on both the Runner target and the NewsWidgets extension in Xcode.",
"The manifest receiver for NewsWidget was not pasted, so its registration is not checked here."
]
}
Cut from this display only (the saved reply and the check use the full text): code (3273 characters, 317 shown).
What step 5 prints for it: B1: widget_sync.dart now passes NewsWidgets, the Widget struct's kind string, as iOSName and keeps NewsWidget as androidName. | B2: NewsWidgets.swift now reads UserDefaults(suiteName: "group.com.example.news"), the App Group the Flutter side sets. | B3: NewsWidget.kt now calls appWidgetManager.updateAppWidget(appWidgetId, views) at the end of each loop pass. | B4: NewsWidget.kt now reads headline_description, the key the Flutter side writes, instead of headline_desc. | B5: NewsWidget.kt now sets the text on R.id.headline_description, the id news_widget.xml defines. | X1: NewsWidgets.swift getTimeline now uses the never policy, since HomeWidget.updateWidget asks for a reload after each save.
The page's check: kept - nothing to flag
review - A step counter that writes one type and reads another
The page read it as: Fix before release - 0 blocker, 5 major, 0 minor; findings B1 not-awaited, B2 not-awaited, B3 forced-read, B4 type-mismatch, B5 android-own-prefs.
Input (code is 2332 characters; the first 1367 are shown; facts is 2304 characters; the first 1394 are shown):
{
"task": "review",
"unit": "Widget: Step counter widget\nPlatforms: iOS and Android\nFiles: lib/steps/step_widget.dart (Dart), ios/StepsWidget/StepsWidget.swift (Swift), android/app/src/main/kotlin/com/example/steps/StepsWidgetProvider.kt (Kotlin/Java)",
"code": "// file: lib/steps/step_widget.dart\nimport 'package:home_widget/home_widget.dart';\n\nclass StepWidgetSync {\n static const group = 'group.com.example.steps';\n\n Future<void> publish(int steps, int goal) async {\n await HomeWidget.setAppGroupId(group);\n HomeWidget.saveWidgetData<int>('steps_today', steps);\n HomeWidget.saveWidgetData<int>('steps_goal', goal);\n await HomeWidget.updateWidget(iOSName: 'StepsWidget', androidName: 'StepsWidgetProvider');\n }\n}\n\n// file: ios/StepsWidget/StepsWidget.swift\nimport WidgetKit\nimport SwiftUI\n\nstruct StepsEntry: TimelineEntry {\n let date: Date\n let steps: String\n}\n\nstruct StepsProvider: TimelineProvider {\n func placeholder(in context: Context) -> StepsEntry { StepsEntry(date: Date(), steps: \"-\") }\n func getSnapshot(in context: Context, completion: @escaping (StepsEntry) -> ()) {\n let defaults = UserDefaults(suiteName: \"group.com.example.steps\")\n let steps = defaults?.string(forKey: \"steps_today\")!\n completion(StepsEntry(date: Date(), steps: steps ?? \"-\"))\n }\n func getTimeline(in context: Context, completion: @escaping (Timeline<StepsEntry>) -> ()) {\n getSnapshot(in: context) { entry in completion(Timeline(entries: [entry], policy: .atEnd)) }\n }\n}\n\nstruct StepsWidget: Widget {\n let kind: String = \"StepsWidget\"\n var body: some WidgetConfiguration {\n\n[... 965 more characters cut from this display ...]",
"carry": "",
"facts": "The page's scan of 62 lines in 3 files (lib/steps/step_widget.dart = Dart, lines 2-14; ios/StepsWidget/StepsWidget.swift = Swift, lines 16-42; android/app/src/main/kotlin/com/example/steps/StepsWidgetProvider.kt = Kotlin/Java, lines 44-62); verdict: Fix before release - 0 blocker, 5 major, 0 minor.\nFindings (id | area | severity | line | where | evidence | source):\n- B1 | dart | major | line 9 | HomeWidget.saveWidgetData | HomeWidget.saveWidgetData is not awaited, so updateWidget can fire before the value is stored and the widget redraws with the old one. | source: HomeWidget.saveWidgetData<int>('steps_today', steps);\n- B2 | dart | major | line 10 | HomeWidget.saveWidgetData | HomeWidget.saveWidgetData is not awaited, so updateWidget can fire before the value is stored and the widget redraws with the old one. | source: HomeWidget.saveWidgetData<int>('steps_goal', goal);\n- B3 | ios | major | line 28 | StepsWidget.swift | The read of 'steps_today' is force-unwrapped - before the app first saves it, the widget extension crashes and shows nothing. | source: let steps = defaults?.string(forKey: \"steps_today\")!\n- B4 | keys | major | line 28 | StepsWidget.swift | 'steps_today' is saved as int (line 9) but read with string - the read returns nil/the default (or throws a ClassCastException on Android). | source: let steps = defaults?.string(forKey: \"steps_today\")!\n- B5 | android |\n\n[... 910 more characters cut from this display ...]"
}
Reply:
{
"task": "review",
"summary": "Today the iOS widget shows a dash or crashes and the Android widget shows 0 steps, since neither reads what the app saves; first read through HomeWidgetPlugin.getData(context) and fix the iOS read.",
"findings": [
{
"id": "B1",
"widget": "step_widget.dart - StepWidgetSync.publish",
"issue": "The widget can redraw with the previous step count because the update fires before the save finishes.",
"fix": "Await the steps_today save before HomeWidget.updateWidget.",
"code": "await HomeWidget.saveWidgetData<int>('steps_today', steps);"
},
{
"id": "B2",
"widget": "step_widget.dart - StepWidgetSync.publish",
"issue": "The goal shown on Android can lag one update behind because its save is not awaited.",
"fix": "Await the steps_goal save before HomeWidget.updateWidget.",
"code": "await HomeWidget.saveWidgetData<int>('steps_goal', goal);"
},
{
"id": "B3",
"widget": "StepsWidget.swift - StepsProvider.getSnapshot",
"issue": "Before the app first saves a value the widget extension crashes on the force-unwrapped read, so the home screen shows nothing.",
"fix": "Drop the force unwrap and fall back with ?? to a placeholder.",
"code": "let steps = defaults?.integer(forKey: \"steps_today\") ?? 0\ncompletion(StepsEntry(date: Date(), steps: String(steps)))"
},
{
"id": "B4",
"widget": "StepsWidget.swift - StepsProvider.getSnapshot",
"issue": "The iOS widget never shows the step count because an int saved by Dart is read with string(forKey:), which returns nil.",
"fix": "Read steps_today with integer(forKey:) and turn it into text for the entry.",
"code": "let steps = defaults?.integer(forKey: \"steps_today\") ?? 0\ncompletion(StepsEntry(date: Date(), steps: String(steps)))"
},
{
"id": "B5",
"widget": "StepsWidgetProvider.kt - StepsWidgetProvider.onUpdate",
"issue": "The Android widget always shows 0 steps and a 0 goal because it reads its own preferences file, not the one home_widget writes.",
"fix": "Get the data with HomeWidgetPlugin.getData(context) and add its import.",
"code": "import es.antonborri.home_widget.HomeWidgetPlugin\n\nval prefs = HomeWidgetPlugin.getData(context)"
}
],
"cleared": [],
"extra": [
{
"id": "X1",
"area": "keys",
"line": 21,
"widget": "StepsWidget.swift - StepsEntry",
"issue": "The app saves steps_goal and Android shows it, but the iOS entry has no goal field, so iOS users never see their goal.",
"fix": "Add a goal field to StepsEntry, read steps_goal with integer(forKey:) and show it in the view."
}
],
"plan": [
"Read the shared data with HomeWidgetPlugin.getData(context) in the Android provider (B5)",
"Remove the force unwrap and read steps_today with integer(forKey:) on iOS (B3, B4)",
"Await both saveWidgetData calls before HomeWidget.updateWidget (B1, B2)",
"Show steps_goal on iOS too (X1)"
],
"caveats": [
"The steps_widget layout was not pasted, so the ids R.id.steps and R.id.goal could not be checked.",
"The AndroidManifest.xml was not pasted, so the StepsWidgetProvider receiver registration could not be checked."
]
}
What step 5 prints for it: B1: Await the steps_today save before HomeWidget.updateWidget. | B2: Await the steps_goal save before HomeWidget.updateWidget. | B3: Drop the force unwrap and fall back with ?? to a placeholder. | B4: Read steps_today with integer(forKey:) and turn it into text for the entry. | B5: Get the data with HomeWidgetPlugin.getData(context) and add its import.
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./bridgekit.js"); require("./bridgereply.js")), then:
const K = BridgeKit, R = BridgeReply;
const f = K.form({ name: "Headline widget", targets: "both", carry: "",
code: "// file: lib/w.dart\nFuture<void> f() async {\n await HomeWidget.saveWidgetData<String>('title', 'x');\n}\n// file: ios/W.swift\nlet t = UserDefaults.standard.string(forKey: \"title\")" });
// f.findings, f.scan.keymap (every key on each side), f.scan.names, f.checklist, f.steps (the native setup steps)
// K.findingsCsv(f), K.keysCsv(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://widget-bridge.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://widget-bridge.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"]
// widget-bridge.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://widget-bridge.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://widget-bridge.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
}
// WidgetBridge.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 WidgetBridge {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://widget-bridge.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://widget-bridge.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://widget-bridge.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://widget-bridge.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":"widget-bridge"}' | 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": "widget-bridge"})["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: "widget-bridge" })).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": "widget-bridge"}, 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", "widget-bridge"), 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: "widget-bridge" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "widget-bridge"])["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 = "widget-bridge" }))!["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", "unit": "Widget: News headline widget
Platforms: iOS and Android
Files: lib/news/widget_sync.dart (Dart), ...", "code": "// file: lib/news/widget_sync.dart
import 'package:home_widget/home_widget.dart';
...", "carry": "", "facts": "The page's scan of 80 lines in 4 files ...
Findings (id | area | severity | line | where | evidence | source):
- B1 | names | major | line 10 | ..."}
# - 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", "unit": "Widget: News headline widget
Platforms: iOS and Android
Files: lib/news/widget_sync.dart (Dart), ...", "code": "// file: lib/news/widget_sync.dart
import 'package:home_widget/home_widget.dart';
...", "carry": "", "facts": "The page's scan of 80 lines in 4 files ...
Findings (id | area | severity | line | where | evidence | source):
- B1 | names | major | line 10 | ..."}
# - 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", unit, 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 widget-bridge:<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="widget-bridge:$(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 = "widget-bridge:" + 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 = `widget-bridge:${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 := "widget-bridge:" + 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 = "widget-bridge:" + 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 = "widget-bridge:#{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 = "widget-bridge:" . $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 = $"widget-bridge:{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: widget-bridge:$(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", "unit": "Widget: News headline widget
Platforms: iOS and Android
Files: lib/news/widget_sync.dart (Dart), ...", "code": "// file: lib/news/widget_sync.dart
import 'package:home_widget/home_widget.dart';
...", "carry": "", "facts": "The page's scan of 80 lines in 4 files ...
Findings (id | area | severity | line | where | evidence | source):
- B1 | names | major | line 10 | ..."}
# - 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": "widget-bridge:" + 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": `widget-bridge:${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", "widget-bridge:"+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", "widget-bridge:" + 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" => "widget-bridge:#{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: widget-bridge:" . 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", $"widget-bridge:{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);