Run Cortex Check from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page lexes the module, finds the functions and the ISRs, runs its lint rules against the context you give - each rule citing the skill section it comes from - lists the NVIC priorities and works out the timing budget, then hands it all to the run as facts. The run writes the review or the corrected module; the page then holds it to its own facts (cortexreply.js), and re-lints the corrected module with the same engine.
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":"cortex-check"}), 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, module, context, facts) is missing. The page always sends all five of task, module, context, 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 - the review, then the fix:
review- one finding per page fail and warn, in the page's order, with its id and line, why and fix; at most 4 extra findings tied to a line; the priorities for the IRQs the page found; the timing budget restated; at least 3 hardware tests.rewrite- the complete corrected module, its changes by id, the chip-specific values to confirm, and a test plan. The page re-lints the code: no fail may remain or be introduced.
A missing or unknown task is answered as review. Both lanes send the same five fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the review into carry for the rewrite lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | review or rewrite. If it is missing or anything else, do review and write "task": "review". |
module | yes | the module as pasted, one line per source line, each prefixed by its line number and " | " (the prefix is not part of the code). |
context | yes | the module's title and purpose, the core, platform, language, RTOS, D-cache, DMA, NVIC priority bits ("not given" means never assume them), clock, 5 V signals and symptom, the functions and ISRs the page found with their lines, the NVIC priorities it found, and the timing budget with every formula. |
carry | send it (empty for review) | only for rewrite: the review carried from the first lane, or one sentence saying nothing was carried. |
facts | yes | the page's checks, one row per line as area | id | status | evidence | where, then the fail and warn rows in order and the IRQs for priorities. A line starting "must:" is a rule your reply has to hold. |
retry_note | no | optional; when present your previous reply could not be used - follow the note. |
The rules the model applies
From the source skill, @sickn33/arm-cortex-expert - the only standard the run applies:
- Safety over performance: correctness first, optimize after profiling. Full solutions: complete drivers with init, ISR and example usage, not snippets. Explain internals: annotate register usage, buffer structures and ISR flows. Document tradeoffs: blocking vs async, RAM vs flash, throughput vs CPU load.
- Safe defaults: guard against buffer overruns, blocking calls, priority inversions and missing barriers. Keep critical sections SHORT (microseconds, not milliseconds); preserve nesting by saving PRIMASK (primask = __get_PRIMASK(); __disable_irq(); ...; __set_PRIMASK(primask)); on M3/M4/M7 prefer BASEPRI when only lower priorities need masking; use atomic operations when feasible instead of disabling interrupts; document the rationale of each critical section in a comment.
- Cortex-M7 (Teensy 4.x, STM32 F7/H7) has weakly-ordered memory: "works with debug prints, fails without them" is a sign of a missing barrier. Use volatile device accesses and the barriers the chip reference manual requires for the operation; barriers do not replace cache maintenance; do not blanket-wrap every MMIO access. In Rust use cortex_m::asm::dmb() and dsb() around volatile accesses.
- DMA and cache coherency on M7: all DMA buffers 32-byte aligned (the cache line), with a size that is a multiple of 32 bytes - violating alignment corrupts adjacent memory during cache invalidate. Placement, best first: 1. DMA-accessible non-cacheable SRAM (a linker section verified accessible to the selected DMA controller; DTCM is often inaccessible to peripheral DMA), 2. an MPU-configured non-cacheable region, 3. cache maintenance as the last resort - clean before DMA reads memory (arm_dcache_flush_delete() or cortex_m::cache::clean_dcache_by_range()), invalidate after DMA writes memory (arm_dcache_delete() or cortex_m::cache::invalidate_dcache_by_range()).
- Write-1-to-clear: many status registers (especially i.MX RT, STM32) clear by writing 1, so status &= ~bit does nothing on a W1C register; write back only the documented W1C bits you intend to acknowledge, and confirm each bit's access type in the reference manual.
- Rust: never use static mut; use an atomic such as AtomicBool for a flag, or Mutex<RefCell<Option<T>>> accessed with critical_section::with; Relaxed for CPU-only, Acquire/Release for shared state.
- NVIC: lower number = higher priority; ISRs at the same level cannot preempt each other; M0/M0+ have 2-4 priority levels, M3/M4/M7 have 8-256 and split preemption from sub-priority by the priority grouping; 0-2 for time-critical work (DMA, timers), 3-7 for normal peripherals (UART, SPI, I2C), 8+ for background tasks.
- Platform gotchas: GPIO max 3.3 V on most platforms (NOT 5 V tolerant except STM32 FT pins) - level shifters for 5 V; Teensy 4.x: FlexSPI dedicated to Flash/PSRAM only, EEPROM emulated (limit writes <10Hz), LPSPI max 30MHz, never change CCM clocks while peripherals are active; STM32 F7/H7: clock domain config per peripheral, fixed DMA stream/channel assignments; nRF52: SAADC needs calibration after power-on, GPIOTE limited (8 channels), the radio shares priority levels; SAMD: SERCOM needs careful pin muxing, GCLK routing is critical, limited DMA on M0+ variants.
- Faults and stack: HardFault causes include unaligned access (especially on M0/M0+), null dereference, stack overflow, illegal instructions and writes to invalid addresses; on M3/M4/M7 read HFSR, CFSR and MMFAR/BFAR and capture the stacked frame (R0-R3, R12, LR, PC, xPSR); M0/M0+ have only HardFault with limited information. Stack protection: MPU guard pages are best, else a canary plus a watchdog. FPU lazy stacking on M4F/M7F makes the latency of an ISR that uses the FPU variable.
- Validate with example usage and notes on timing, latency and throughput. Hardware limits, memory maps, cache maintenance, NVIC priority bits, clocks and pin tolerance depend on the exact MCU revision and board: never assume them, and say what must be confirmed in the datasheet before flashing or energizing connected hardware.
House rules
- Use only the input. The page's checks are decided: never drop, merge or re-rate a page row, and give every page row its exact id. A hazard the page did not raise is an extra finding with an id starting extra- and a line number.
- Never invent a hardware number, register fact, address, pin, clock, IRQ number or limit. Every chip-specific value you use goes in caveats or assumptions as something to confirm in the datasheet or reference manual. When NVIC priority bits are "not given", never assume a range.
- Never claim to have compiled, flashed, built, run or tested anything: you have only read the module.
- Never a link, a URL or a web address. Never use the ` character (backtick) in a text field; name a function or register plainly, as in __DSB() or USART2->SR. Never refer to the input as data or fields.
- Ids exactly as the page writes them: isr-blocking, isr-rtos, shared-volatile, static-mut, dma-align, dma-cache, mmio-barrier, w1c, crit-nesting, crit-basepri, crit-basepri-m0, crit-long, nvic, nvic-band, nvic-equal, nvic-raw, unaligned-m0, fpu-isr, teensy-lpspi, teensy-eeprom, teensy-ccm, teensy-flexspi, nrf-saadc-cal, nrf-gpiote, samd-sercom, samd-gclk, samd-dma, stm32-dma-map, volt, fault, stack, timing, ctx-isr, ctx-dcache, ctx-nvic-bits, ctx-parse, ctx-lang. Terms from the rules above are free to use; never paste a whole sentence of this prompt.
- Reply with ONLY one JSON object - nothing before or after it, no code fences.
The replies
review
Review the module the way the skill would, finding by finding, from the page's checks.
summary- one or two sentences, at most 60 words: what the module does, the page's verdict, and what to fix first.findings- one object per page fail and warn row, in the page's order:id(the page's id),where(the function and line exactly as the page gave them, for example "onDmaDone(), line 26"; "module" or "context" when the page gave no line),why(one sentence: the mechanism - what goes wrong on the hardware and when) andfix(one sentence: the change, in the skill's terms). After them, at most 4 extra findings the page did not raise, each with an id starting extra- and a where naming a line of the module.priorities- one object per IRQ the page listed for priorities, none when it listed none:irq(as the page wrote it),current(as set in the module),proposed(a number; within the range the page gives when NVIC priority bits are known, 0-3 on M0/M0+) andwhy(one sentence: the skill's band, preemption between equal levels, any RTOS ceiling to confirm).timing- one sentence restating the page's timing budget (ISR time, CPU load, time to fill the buffer, the main loop's drain interval) using only the page's numbers, written as the page writes them; when the budget is not filled in, say so and name what is missing, with no number.hardware_tests- at least 3 short sentences: what to run on the board and what observation proves the fix (a logic analyser on the chip select, a GPIO toggled around the ISR on a scope, a stress run with the prints removed, a fault injected on purpose).caveats- short sentences: the datasheet, reference manual and silicon revision values to confirm.
{
"task": "review",
"summary": "one or two sentences",
"findings": [ { "id": "isr-blocking", "where": "USART2_IRQHandler(), line 20", "why": "one sentence", "fix": "one sentence" }, { "id": "extra-overrun", "where": "USART2_IRQHandler(), line 24", "why": "one sentence", "fix": "one sentence" } ],
"priorities": [ { "irq": "USART2_IRQn", "current": "6", "proposed": "5", "why": "one sentence" } ],
"timing": "one sentence",
"hardware_tests": [ "one", "two", "three" ],
"caveats": [ "one sentence" ]
}
The page holds it to its own facts (check() in cortexreply.js; any warning makes the reply flagged): the task it was sent; no web link; no claim of having compiled, flashed, built, run or tested anything; no backticks; no input field named; at least one caveat; a summary of at most 60 words; one finding per page fail and warn, matched by id and line, in the page's order; every finding with where, why and fix; extra findings only with an extra- id, at most 4, each on a line of the module; no other id the page did not raise; priorities only for the IRQs the page found, one row for each, a numeric proposal inside 0 to 2^bits - 1 when the bits are given (0-3 on M0/M0+); a timing sentence whose every number appears in the page's budget; at least 3 hardware tests.
rewrite
Write the corrected module from the carried review and the page's checks.
filename- a file name with the language's extension (.c or .h for C, .cpp or .ino for C++, .rs for Rust).language- the page's language id: c, cpp or rust.code- the complete corrected module as plain source text, with no line-number prefixes and no code fences: init, ISRs and the user-facing API, keeping every ISR the page found under its name. Fix every fail the page found - the page runs its own lint on this code with the same context and flags any fail that remains or is new - and fix the warnings where the review says how. Comment each critical section, barrier, cache maintenance call and alignment with its reason. Keep register names and IRQ names as the module has them; mark any value you had to choose with a comment saying it must be confirmed.changes- objects withid(a page id, or an extra- id from the carried review) andwhat(one sentence: what changed and why); every page fail id appears.assumptions- short sentences: every chip-specific value the person must confirm (register and bit names, IRQ numbers, linker section names, clock values, the buffer placement, the priority ceiling).test_plan- at least 2 short sentences: the bench steps that prove the module, in order.caveats- short sentences: what the rewrite could not decide from the input.
{
"task": "rewrite",
"filename": "uart_rx.c",
"language": "c",
"code": "the complete module",
"changes": [ { "id": "isr-rtos", "what": "one sentence" } ],
"assumptions": [ "one sentence" ],
"test_plan": [ "one", "two" ],
"caveats": [ "one sentence" ]
}
The page requires: the task it was sent; the same text rules; the page's language and a file name that fits it; code without Markdown fences or line-number prefixes; balanced braces, brackets and parentheses; the page's own lint, re-run on the code with the same context, finding no fail - one still there or a new one is flagged; every ISR the page found still there; at least half the module's lines; no static mut in Rust; every page fail id in changes, and every change id either a page id or an extra- id from the carried review; at least one assumption and two test plan steps.
Worked examples
Three runs from the page's free samples (invented modules): the review of the Teensy 4.1 SPI DMA reader, the corrected module for the same reader carrying that review, and the review of the STM32F4 UART receiver, which has warnings only. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but long fields 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 - Teensy 4.1 SPI DMA reader
The page read it as: fix first - failing hazards found on Cortex-M7F, Teensy 4.x (i.MX RT1062); ISRs onDmaDone; fails: isr-blocking, dma-align, dma-align, teensy-lpspi; warns: mmio-barrier, shared-volatile, dma-cache, crit-nesting, crit-long.
Input (module is 2188 characters; the first 1397 are shown; facts is 3790 characters; the first 1396 are shown):
{
"task": "review",
"module": " 1 | // IMU burst reader for Teensy 4.1 - LPSPI4 with two DMA channels, 100-byte frames\n 2 | #include <Arduino.h>\n 3 | #include <SPI.h>\n 4 | #include <DMAChannel.h>\n 5 | \n 6 | #define FRAME_LEN 100\n 7 | #define RING_LEN 256\n 8 | #define CS_PIN 10\n 9 | \n10 | static uint8_t tx_frame[FRAME_LEN];\n11 | static uint8_t rx_frame[FRAME_LEN];\n12 | DMAChannel dmaTx;\n13 | DMAChannel dmaRx;\n14 | \n15 | static uint8_t ring[RING_LEN];\n16 | static volatile uint16_t ring_head = 0;\n17 | static volatile bool frame_ready = false;\n18 | static uint32_t frames_seen = 0;\n19 | \n20 | void onDmaDone() {\n21 | dmaRx.clearInterrupt();\n22 | digitalWriteFast(CS_PIN, HIGH);\n23 | SPI.endTransaction();\n24 | frames_seen++;\n25 | frame_ready = true;\n26 | Serial.printf(\"frame %lu\\n\", frames_seen); // remove this and frames go stale\n27 | }\n28 | \n29 | void startFrame() {\n30 | SPI.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE3));\n31 | digitalWriteFast(CS_PIN, LOW);\n32 | dmaTx.sourceBuffer(tx_frame, FRAME_LEN);\n33 | dmaTx.destination((volatile uint8_t &)LPSPI4_TDR);\n34 | dmaTx.triggerAtHardwareEvent(DMAMUX_SOURCE_LPSPI4_TX);\n35 | dmaRx.source((volatile uint8_t &)LPSPI4_RDR);\n36 | dmaRx.destinationBuffer(rx_frame, FRAME_LEN);\n37 | dmaRx.triggerAtHardwareEvent(DMAMUX_SOURCE_LPSPI4_RX);\n38 | dmaRx.interruptAtCompletion();\n39 | dmaRx.enable();\n40 | dmaTx.enable();\n41 | }\n42 |\n\n[... 791 more characters cut from this display ...]",
"context": "Module: IMU burst reader on LPSPI4 with DMA\nPurpose: Reads a 100-byte burst from an IMU over SPI with two DMA channels, 2000 times a second, and copies each frame into a ring buffer the main loop drains. Works with the Serial prints in place; drops frames and reads stale bytes when they are removed.\nCore: m7f - Cortex-M7F; platform: teensy4 - Teensy 4.x (i.MX RT1062); language: cpp - C++\nRTOS: none (bare metal); D-cache: on; DMA: yes; NVIC priority bits: not given - they depend on the exact MCU, never assume them; clock: 600 MHz; 5 V signals: no; symptom: works with debug prints, fails without them\nFunctions the page found: onDmaDone (line 20), startFrame (line 29), pushFrame (line 43), setup (line 52), loop (line 63)\nISRs the page found: onDmaDone (attachInterrupt(), line 20)\nNVIC priorities the page found: none\n\nTiming budget (pure arithmetic from the numbers given, no chip claims; missing: nothing):\n- ISR time: cycles / clock = 1800 / 600 MHz = 3 us\n- CPU load from this ISR: rate x cycles / (clock x 10^6) x 100 = 2000 x 1800 / (600 x 10^6) x 100 = 0.6 %\n- Incoming bytes: as given = 200000 bytes/s\n- Time to fill the buffer: buffer / bytes per second = 256 / 200000 x 1000 = 1.28 ms\n- Main loop's maximum drain interval: the buffer must be emptied before it fills = time to fill = 1.28 ms",
"carry": "",
"facts": "Page checks (4 fail, 5 warn, 3 info, 0 ok, 0 n/a); verdict: fix first - failing hazards found. Each row: area | id | status | evidence | where.\n- Interrupts | isr-blocking | fail | onDmaDone calls Serial.printf - a Serial print inside an ISR; the skill's safe defaults guard against blocking calls | onDmaDone(), line 26\n- DMA and cache | dma-align | fail | tx_frame is a DMA buffer without 32-byte alignment; 100 bytes is not a multiple of 32 (round up to 128) - violating alignment corrupts adjacent memory during cache invalidate | line 10\n- DMA and cache | dma-align | fail | rx_frame is a DMA buffer without 32-byte alignment; 100 bytes is not a multiple of 32 (round up to 128) - violating alignment corrupts adjacent memory during cache invalidate | line 11\n- Platform | teensy-lpspi | fail | SPISettings at 40000000 Hz (40 MHz) - the skill: LPSPI max 30MHz on Teensy 4.x | startFrame(), line 30\n- Registers and barriers | mmio-barrier | warn | the symptom is the skill's first sign of a missing barrier on the weakly-ordered M7: works with debug prints, fails without them (the print adds an implicit delay) - add the barrier the reference manual requires, do not blanket-wrap every MMIO access | context (symptom)\n- Shared data and critical sections | shared-volatile | warn | frames_seen, declared at line 18, is written in onDmaDone and read in loop() (line 67) but is neither volatile\n\n[... 2394 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "review",
"summary": "An IMU burst reader on Teensy 4.1 that moves 100-byte frames over LPSPI4 with two DMA channels. The page says fix first: a Serial.printf in the DMA-complete ISR, two unaligned DMA buffers with no cache maintenance, and SPI clocked at 40 [... 61 more characters cut from this display]",
"findings": [
{
"id": "isr-blocking",
"where": "onDmaDone(), line 26",
"why": "Serial.printf formats text and can wait for room in the USB serial buffer inside the DMA-complete interrupt, so the ISR's length depends on the host and every equal or lower priority interrupt waits behind it.",
"fix": "Only set frame_ready and count the frame in onDmaDone, and print from loop()."
},
{
"id": "dma-align",
"where": "line 10",
"why": "On the M7 cache maintenance works on whole 32-byte lines, so an unaligned 100-byte tx_frame shares lines with its neighbours and a clean or invalidate on it can corrupt them.",
"fix": "Declare tx_frame with __attribute__((aligned(32))) and a size rounded up to 128 bytes, still transferring only FRAME_LEN bytes."
},
{
"id": "dma-align",
"where": "line 11",
"why": "rx_frame is invalidated after DMA writes it, and with 100 unaligned bytes that invalidate also throws away whatever the CPU last wrote to the variables sharing its first and last cache lines.",
"fix": "Declare rx_frame with __attribute__((aligned(32))) and 128 bytes, the next multiple of 32."
},
{
"id": "teensy-lpspi",
"where": "startFrame(), line 30",
"why": "40 MHz is above the LPSPI maximum of 30MHz the skill gives for Teensy 4.x, so the link runs outside its rated speed and reads can be marginal.",
"fix": "Use SPISettings(30000000, MSBFIRST, SPI_MODE3) or lower, and stay inside the sensor's own maximum clock."
},
{
"id": "mmio-barrier",
"where": "context (symptom)",
"why": "A failure that goes away when a print is added is the skill's first symptom of reordered accesses on the weakly-ordered M7, and here the print also delays the restart long enough to hide the stale cache lines.",
"fix": "Fix the cache maintenance first, then add only the barrier the reference manual requires, such as a __DSB() between the cache maintenance and enabling the channels, rather than wrapping every register access."
},
{
"id": "shared-volatile",
"where": "onDmaDone(), line 24",
"why": "frames_seen is incremented in onDmaDone and read in loop(), and without volatile the compiler may keep a copy in a register in loop() and print a stale count.",
"fix": "Declare frames_seen volatile (or std::atomic<uint32_t>) and read it once into a local in loop()."
},
{
"id": "dma-cache",
"where": "startFrame(), line 32",
"why": "With the D-cache on, DMA reads tx_frame from RAM while the CPU's last writes may still sit in the cache, and the CPU reads rx_frame from cache lines that DMA bypassed, so both directions can see stale bytes.",
"fix": "Prefer a DMA-accessible non-cacheable region if the memory map has one; otherwise call arm_dcache_flush_delete on tx_frame before enabling DMA and arm_dcache_delete on rx_frame once the transfer completes."
},
{
"id": "crit-nesting",
"where": "pushFrame(), line 44",
"why": "__enable_irq() at the end turns interrupts on unconditionally, so a caller that already had them masked gets them back on in the middle of its own critical section.",
"fix": "Save PRIMASK with __get_PRIMASK() before __disable_irq() and restore it with __set_PRIMASK(primask)."
},
{
"id": "crit-long",
"where": "pushFrame(), line 44",
"why": "The 100-iteration copy runs with every interrupt masked, which holds off the DMA-complete interrupt and everything else for the whole copy.",
"fix": "Copy the frame with interrupts enabled and mask only the publish of the new ring_head, since only loop() writes the ring."
},
{
"id": "extra-ring-overrun",
"where": "pushFrame(), line 46",
"why": "The ring has a head index and no tail, so nothing stops pushFrame from overwriting bytes a consumer has not read yet.",
"fix": "Add a tail index owned by the consumer and drop and count a frame when fewer than FRAME_LEN bytes are free."
}
],
"priorities": [],
"timing": "The page's budget: the ISR takes 3 us, which at 2000 Hz is 0.6 % of the CPU, and at 200000 bytes/s the 256-byte ring fills in 1.28 ms, so the main loop must drain it at least every 1.28 ms.",
"hardware_tests": [
"Remove every Serial print from the ISR path and run a long capture, checking a register whose value is known in every frame to prove no frame is stale.",
"Loop MOSI back to MISO, fill tx_frame with a counting pattern and compare rx_frame byte for byte after each transfer to prove the alignment and the cache maintenance.",
"Toggle a spare GPIO at the start and end of onDmaDone and measure the pulse on a scope against the page's ISR time.",
"Measure SCK on a logic analyser to confirm the clock is at or below 30 MHz."
],
"caveats": [
"Confirm where the linker places tx_frame and rx_frame (DTCM, OCRAM through DMAMEM, or another region), whether that region is cached, and whether the eDMA can reach it - the skill notes DTCM is often inaccessible to peripheral DMA.",
"Confirm the sensor's own maximum SPI clock and mode in its datasheet.",
"The module sets no NVIC priority, so the DMA channel interrupt runs at whatever level the library gives it."
]
}
Cut from this display only (the saved reply and the check use the full text): summary (297 characters, 236 shown).
What step 5 prints for it: isr-blocking: onDmaDone(), line 26 | dma-align: line 10 | dma-align: line 11 | teensy-lpspi: startFrame(), line 30 | mmio-barrier: context (symptom) | shared-volatile: onDmaDone(), line 24 | dma-cache: startFrame(), line 32 | crit-nesting: pushFrame(), line 44 | crit-long: pushFrame(), line 44 | extra-ring-overrun: pushFrame(), line 46
The page's check: kept - nothing to flag
rewrite - Teensy 4.1 SPI DMA reader
The input carries the saved review (R.laneForm fills carry from the review reply), as the page's handoff does. The page's lint on the returned code: 0 fail, 1 warn.
Input (module is 2188 characters; the first 1397 are shown; carry is 2651 characters; the first 1396 are shown; facts is 3734 characters; the first 1396 are shown):
{
"task": "rewrite",
"module": " 1 | // IMU burst reader for Teensy 4.1 - LPSPI4 with two DMA channels, 100-byte frames\n 2 | #include <Arduino.h>\n 3 | #include <SPI.h>\n 4 | #include <DMAChannel.h>\n 5 | \n 6 | #define FRAME_LEN 100\n 7 | #define RING_LEN 256\n 8 | #define CS_PIN 10\n 9 | \n10 | static uint8_t tx_frame[FRAME_LEN];\n11 | static uint8_t rx_frame[FRAME_LEN];\n12 | DMAChannel dmaTx;\n13 | DMAChannel dmaRx;\n14 | \n15 | static uint8_t ring[RING_LEN];\n16 | static volatile uint16_t ring_head = 0;\n17 | static volatile bool frame_ready = false;\n18 | static uint32_t frames_seen = 0;\n19 | \n20 | void onDmaDone() {\n21 | dmaRx.clearInterrupt();\n22 | digitalWriteFast(CS_PIN, HIGH);\n23 | SPI.endTransaction();\n24 | frames_seen++;\n25 | frame_ready = true;\n26 | Serial.printf(\"frame %lu\\n\", frames_seen); // remove this and frames go stale\n27 | }\n28 | \n29 | void startFrame() {\n30 | SPI.beginTransaction(SPISettings(40000000, MSBFIRST, SPI_MODE3));\n31 | digitalWriteFast(CS_PIN, LOW);\n32 | dmaTx.sourceBuffer(tx_frame, FRAME_LEN);\n33 | dmaTx.destination((volatile uint8_t &)LPSPI4_TDR);\n34 | dmaTx.triggerAtHardwareEvent(DMAMUX_SOURCE_LPSPI4_TX);\n35 | dmaRx.source((volatile uint8_t &)LPSPI4_RDR);\n36 | dmaRx.destinationBuffer(rx_frame, FRAME_LEN);\n37 | dmaRx.triggerAtHardwareEvent(DMAMUX_SOURCE_LPSPI4_RX);\n38 | dmaRx.interruptAtCompletion();\n39 | dmaRx.enable();\n40 | dmaTx.enable();\n41 | }\n42 |\n\n[... 791 more characters cut from this display ...]",
"context": "Module: IMU burst reader on LPSPI4 with DMA\nPurpose: Reads a 100-byte burst from an IMU over SPI with two DMA channels, 2000 times a second, and copies each frame into a ring buffer the main loop drains. Works with the Serial prints in place; drops frames and reads stale bytes when they are removed.\nCore: m7f - Cortex-M7F; platform: teensy4 - Teensy 4.x (i.MX RT1062); language: cpp - C++\nRTOS: none (bare metal); D-cache: on; DMA: yes; NVIC priority bits: not given - they depend on the exact MCU, never assume them; clock: 600 MHz; 5 V signals: no; symptom: works with debug prints, fails without them\nFunctions the page found: onDmaDone (line 20), startFrame (line 29), pushFrame (line 43), setup (line 52), loop (line 63)\nISRs the page found: onDmaDone (attachInterrupt(), line 20)\nNVIC priorities the page found: none\n\nTiming budget (pure arithmetic from the numbers given, no chip claims; missing: nothing):\n- ISR time: cycles / clock = 1800 / 600 MHz = 3 us\n- CPU load from this ISR: rate x cycles / (clock x 10^6) x 100 = 2000 x 1800 / (600 x 10^6) x 100 = 0.6 %\n- Incoming bytes: as given = 200000 bytes/s\n- Time to fill the buffer: buffer / bytes per second = 256 / 200000 x 1000 = 1.28 ms\n- Main loop's maximum drain interval: the buffer must be emptied before it fills = time to fill = 1.28 ms",
"carry": "Summary: An IMU burst reader on Teensy 4.1 that moves 100-byte frames over LPSPI4 with two DMA channels. The page says fix first: a Serial.printf in the DMA-complete ISR, two unaligned DMA buffers with no cache maintenance, and SPI clocked at 40 MHz. Fix the buffers and the ISR print before anything else.\nFindings: isr-blocking (onDmaDone(), line 26) - Only set frame_ready and count the frame in onDmaDone, and print from loop(); dma-align (line 10) - Declare tx_frame with __attribute__((aligned(32))) and a size rounded up to 128 bytes, still transferring only FRAME_LEN bytes; dma-align (line 11) - Declare rx_frame with __attribute__((aligned(32))) and 128 bytes, the next multiple of 32; teensy-lpspi (startFrame(), line 30) - Use SPISettings(30000000, MSBFIRST, SPI_MODE3) or lower, and stay inside the sensor's own maximum clock; mmio-barrier (context (symptom)) - Fix the cache maintenance first, then add only the barrier the reference manual requires, such as a __DSB() between the cache maintenance and enabling the channels, rather than wrapping every register access; shared-volatile (onDmaDone(), line 24) - Declare frames_seen volatile (or std::atomic<uint32_t>) and read it once into a local in loop(); dma-cache (startFrame(), line 32) - Prefer a DMA-accessible non-cacheable region if the memory map has one; otherwise call arm_dcache_flush_delete on tx_frame before enabling\n\n[... 1255 more characters cut from this display ...]",
"facts": "Page checks (4 fail, 5 warn, 3 info, 0 ok, 0 n/a); verdict: fix first - failing hazards found. Each row: area | id | status | evidence | where.\n- Interrupts | isr-blocking | fail | onDmaDone calls Serial.printf - a Serial print inside an ISR; the skill's safe defaults guard against blocking calls | onDmaDone(), line 26\n- DMA and cache | dma-align | fail | tx_frame is a DMA buffer without 32-byte alignment; 100 bytes is not a multiple of 32 (round up to 128) - violating alignment corrupts adjacent memory during cache invalidate | line 10\n- DMA and cache | dma-align | fail | rx_frame is a DMA buffer without 32-byte alignment; 100 bytes is not a multiple of 32 (round up to 128) - violating alignment corrupts adjacent memory during cache invalidate | line 11\n- Platform | teensy-lpspi | fail | SPISettings at 40000000 Hz (40 MHz) - the skill: LPSPI max 30MHz on Teensy 4.x | startFrame(), line 30\n- Registers and barriers | mmio-barrier | warn | the symptom is the skill's first sign of a missing barrier on the weakly-ordered M7: works with debug prints, fails without them (the print adds an implicit delay) - add the barrier the reference manual requires, do not blanket-wrap every MMIO access | context (symptom)\n- Shared data and critical sections | shared-volatile | warn | frames_seen, declared at line 18, is written in onDmaDone and read in loop() (line 67) but is neither volatile\n\n[... 2338 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "rewrite",
"filename": "imu_burst_reader.ino",
"language": "cpp",
"code": "// IMU burst reader for Teensy 4.1 - LPSPI4 with two DMA channels, 100-byte frames\n// Corrected: 32-byte aligned DMA buffers sized to whole cache lines, cache maintenance in both directions,\n// no blocking call in the DMA-complete ISR, SPI [... 3582 more characters cut from this display]",
"changes": [
{
"id": "isr-blocking",
"what": "onDmaDone no longer calls Serial.printf; it counts the frame, sets frame_ready, and loop() prints."
},
{
"id": "dma-align",
"what": "tx_frame and rx_frame are declared __attribute__((aligned(32))) and 128 bytes (FRAME_BUF), while each transfer still moves FRAME_LEN bytes."
},
{
"id": "teensy-lpspi",
"what": "SPISettings uses SPI_HZ, 30000000, the LPSPI maximum the skill gives for Teensy 4.x."
},
{
"id": "dma-cache",
"what": "startFrame cleans tx_frame with arm_dcache_flush_delete before enabling DMA, and onDmaDone invalidates rx_frame with arm_dcache_delete once DMA has written it."
},
{
"id": "mmio-barrier",
"what": "A __DSB() separates the cache maintenance from enabling the channels; the symptom stays a warning until the bench test without prints passes."
},
{
"id": "shared-volatile",
"what": "frames_seen is volatile and loop() reads it once into a local."
},
{
"id": "crit-nesting",
"what": "pushFrame saves PRIMASK with __get_PRIMASK() and restores it with __set_PRIMASK(primask) instead of calling __enable_irq()."
},
{
"id": "crit-long",
"what": "The copy runs with interrupts enabled; the critical section holds only the store of the new ring_head."
},
{
"id": "extra-ring-overrun",
"what": "The ring gains a consumer-owned ring_tail and ringRead(), and pushFrame drops and counts a frame when there is no room."
}
],
"assumptions": [
"tx_frame and rx_frame stay where the linker puts them; confirm that region is reachable by the eDMA and whether it is cached, or move them to a DMA-accessible non-cacheable region.",
"arm_dcache_flush_delete and arm_dcache_delete take an address and a byte count in the Teensy core in use.",
"LPSPI4_TDR, LPSPI4_RDR and DMAMUX_SOURCE_LPSPI4_TX/RX are the names for the SPI port wired to the sensor.",
"30 MHz and SPI_MODE3 are within the sensor's datasheet limits, and the chip select is on pin 10.",
"The __DSB() placement is a choice to confirm against the reference manual for this operation."
],
"test_plan": [
"Run with no Serial print in the ISR path and check a known register value in every frame over a long capture.",
"Loop MOSI to MISO with a counting pattern in tx_frame and compare rx_frame byte for byte after every transfer.",
"Hold the consumer off on purpose and confirm frames_dropped rises while ring contents stay intact.",
"Measure SCK and the onDmaDone width on a logic analyser."
],
"caveats": [
"The buffer placement decides whether cache maintenance is needed at all; the page could not see the linker map.",
"The DMA channel interrupt priority is left to the library, as in the module."
]
}
Cut from this display only (the saved reply and the check use the full text): code (3821 characters, 239 shown).
What step 5 prints for it: imu_burst_reader.ino: 9 changes
The page's check: kept - nothing to flag
review - STM32F4 UART ring buffer, FreeRTOS
The page read it as: review - warnings only on Cortex-M4F, STM32 F4; ISRs USART2_IRQHandler, TIM2_IRQHandler; fails: none; warns: isr-rtos, shared-volatile, shared-volatile, w1c.
Input (module is 2538 characters; the first 1399 are shown; context is 1499 characters; the first 1395 are shown; facts is 3049 characters; the first 1391 are shown):
{
"task": "review",
"module": " 1 | /* USART2 line receiver for an STM32F407 under FreeRTOS: bytes into a ring buffer, one queue event per line */\n 2 | #include \"stm32f4xx_hal.h\"\n 3 | #include \"FreeRTOS.h\"\n 4 | #include \"queue.h\"\n 5 | \n 6 | #define RX_LEN 128\n 7 | #define USART2_BRR 0x16D /* 115200 baud at this board's APB1 clock */\n 8 | \n 9 | static uint8_t rx_buf[RX_LEN];\n10 | static uint16_t rx_head = 0;\n11 | static uint16_t rx_tail = 0;\n12 | static volatile uint32_t tick_ms = 0;\n13 | static QueueHandle_t line_queue;\n14 | \n15 | extern void handle_line(const uint8_t *line, int n);\n16 | \n17 | void USART2_IRQHandler(void)\n18 | {\n19 | uint32_t sr = USART2->SR;\n20 | if (sr & USART_SR_RXNE) {\n21 | uint8_t c = (uint8_t)(USART2->DR & 0xFF);\n22 | rx_buf[rx_head] = c;\n23 | rx_head = (rx_head + 1) % RX_LEN;\n24 | if (c == '\\n') {\n25 | uint8_t evt = 1;\n26 | xQueueSend(line_queue, &evt, 0);\n27 | }\n28 | }\n29 | if (sr & USART_SR_ORE) {\n30 | USART2->SR &= ~USART_SR_ORE;\n31 | }\n32 | }\n33 | \n34 | void TIM2_IRQHandler(void)\n35 | {\n36 | if (TIM2->SR & TIM_SR_UIF) {\n37 | TIM2->SR = ~TIM_SR_UIF;\n38 | tick_ms++;\n39 | }\n40 | }\n41 | \n42 | uint32_t uart_ms(void)\n43 | {\n44 | return tick_ms;\n45 | }\n46 | \n47 | int uart_read_line(uint8_t *out, int max)\n48 | {\n49 | int n = 0;\n50 | while (rx_tail != rx_head &&\n\n[... 1139 more characters cut from this display ...]",
"context": "Module: USART2 line receiver under FreeRTOS\nPurpose: Receives command lines at 115200 baud into a ring buffer from the USART2 interrupt and wakes a FreeRTOS task once per newline. Under load a line is now and then lost or repeated.\nCore: m4f - Cortex-M4F; platform: stm32f4 - STM32 F4; language: c - C\nRTOS: FreeRTOS; D-cache: unknown (only M7 has caches); DMA: no; NVIC priority bits: 4 (levels 0-15); clock: 168 MHz; 5 V signals: no; symptom: intermittent failures\nFunctions the page found: USART2_IRQHandler (line 17), TIM2_IRQHandler (line 34), uart_ms (line 42), uart_read_line (line 47), uart_rx_init (line 59), uart_task (line 76)\nISRs the page found: USART2_IRQHandler (*_IRQHandler name, line 17), TIM2_IRQHandler (*_IRQHandler name, line 34)\nNVIC priorities the page found: USART2_IRQn - HAL_NVIC_SetPriority 6 / sub 0 at line 70 (3-7 normal peripherals); TIM2_IRQn - HAL_NVIC_SetPriority 6 / sub 0 at line 71 (3-7 normal peripherals)\n\nTiming budget (pure arithmetic from the numbers given, no chip claims; missing: nothing):\n- ISR time: cycles / clock = 400 / 168 MHz = 2.38 us\n- CPU load from this ISR: rate x cycles / (clock x 10^6) x 100 = 11520 x 400 / (168 x 10^6) x 100 = 2.74 %\n- Incoming bytes: baud / 10 = 115200 / 10 (assumes 8N1: start + 8 data + stop = 10 bits per byte) = 11520 bytes/s\n- Time to fill the buffer: buffer / bytes per second = 128 / 11520 x 1000 = 11.1 ms\n-\n\n[... 104 more characters cut from this display ...]",
"carry": "",
"facts": "Page checks (0 fail, 4 warn, 5 info, 1 ok, 0 n/a); verdict: review - warnings only. Each row: area | id | status | evidence | where.\n- Interrupts | isr-rtos | warn | USART2_IRQHandler calls xQueueSend - FreeRTOS gives interrupts the FromISR form (xQueueSendFromISR, with the higher-priority-task-woken flag); RTOS API rule, not in the skill | USART2_IRQHandler(), line 26\n- Shared data and critical sections | shared-volatile | warn | rx_buf (an array: its elements), declared at line 9, is written in USART2_IRQHandler and read in uart_read_line() (line 51) but is neither volatile nor atomic - the compiler may cache or reorder it | USART2_IRQHandler(), line 22\n- Shared data and critical sections | shared-volatile | warn | rx_head, declared at line 10, is written in USART2_IRQHandler and read in uart_read_line() (line 50) but is neither volatile nor atomic - the compiler may cache or reorder it | USART2_IRQHandler(), line 23\n- Registers and barriers | w1c | warn | USART2->SR is cleared with a read-modify-write that writes 0 to the bit - many status registers (especially i.MX RT, STM32) clear by writing 1, and status &= ~bit does nothing on W1C registers; confirm this bit's access type in the reference manual | USART2_IRQHandler(), line 30\n- NVIC priorities | nvic-band | info | USART2_IRQn at priority 6 (sub 0): the skill's 3-7 normal peripherals band (0-2 DMA and timers, 3-7\n\n[... 1658 more characters cut from this display ...]"
}
Reply (long strings shortened for display - see the cuts below):
{
"task": "review",
"summary": "A USART2 line receiver on an STM32F4 under FreeRTOS that fills a ring buffer from the interrupt and wakes a task per newline. The page found warnings only: the non-FromISR queue call, an index and buffer shared without volatile, and the [... 85 more characters cut from this display]",
"findings": [
{
"id": "isr-rtos",
"where": "USART2_IRQHandler(), line 26",
"why": "xQueueSend is the task-level call, so from the interrupt it can try to block or switch context and corrupt the scheduler's state, which fits the lost and repeated lines.",
"fix": "Call xQueueSendFromISR with a higher-priority-task-woken flag and end the ISR with portYIELD_FROM_ISR(woken)."
},
{
"id": "shared-volatile",
"where": "USART2_IRQHandler(), line 22",
"why": "rx_buf is written in the interrupt and read in uart_read_line, and without volatile the compiler may read its elements early or reuse a stale copy around the index check.",
"fix": "Declare rx_buf volatile, or keep it plain and order the accesses with a compiler barrier around the index update."
},
{
"id": "shared-volatile",
"where": "USART2_IRQHandler(), line 23",
"why": "rx_head is advanced in the interrupt and read in the uart_read_line loop, and without volatile the loop may test a value held in a register and never see new bytes, or see them late.",
"fix": "Declare rx_head and rx_tail volatile and read rx_head once per pass into a local."
},
{
"id": "w1c",
"where": "USART2_IRQHandler(), line 30",
"why": "USART2->SR &= ~USART_SR_ORE writes 0 to the overrun bit through a read-modify-write, which does nothing if the bit does not clear that way and also writes back every other flag it just read.",
"fix": "Clear the overrun the way the reference manual documents for this bit, such as reading DR after SR, and never read-modify-write the status register."
},
{
"id": "extra-ring-full",
"where": "USART2_IRQHandler(), line 23",
"why": "The interrupt advances rx_head without checking it against rx_tail, so a full ring silently overwrites the oldest unread line.",
"fix": "Compute the next head, drop and count the byte when it equals rx_tail, and expose the count."
}
],
"priorities": [
{
"irq": "USART2_IRQn",
"current": "6 (sub 0)",
"proposed": "5",
"why": "A normal peripheral in the skill's 3-7 band; because it calls a FromISR API it must stay at or below the FreeRTOS syscall ceiling, configMAX_SYSCALL_INTERRUPT_PRIORITY, which is to be confirmed in FreeRTOSConfig.h."
},
{
"irq": "TIM2_IRQn",
"current": "6 (sub 0)",
"proposed": "7",
"why": "A millisecond counter is background work here, and at the same level as USART2 neither can preempt the other, so moving it one level lower lets the receiver preempt it."
}
],
"timing": "The page's budget: each USART2 interrupt takes 2.38 us at 168 MHz, 2.74 % of the CPU at 11520 interrupts a second, and at 11520 bytes/s (115200 baud, 10 bits per byte) the 128-byte ring fills in 11.1 ms, so the task must drain it at least [... 15 more characters cut from this display]",
"hardware_tests": [
"Stream a file of numbered lines at 115200 baud for an hour and have the task check that no line number is skipped or repeated.",
"Hold the task off on purpose for longer than the page's drain interval and confirm the overrun counter rises instead of lines being corrupted.",
"Toggle a GPIO around USART2_IRQHandler and measure its width and jitter on a scope while TIM2 runs.",
"Force an overrun by pausing interrupts briefly and confirm ORE clears and reception continues."
],
"caveats": [
"Confirm in the STM32F4 reference manual how ORE is cleared on this part and which SR bits are read-only.",
"Confirm configMAX_SYSCALL_INTERRUPT_PRIORITY and the priority grouping used by this FreeRTOS port before changing the priorities.",
"The BRR value and the timer prescaler depend on this board's clock tree; the page did not check them."
]
}
Cut from this display only (the saved reply and the check use the full text): summary (321 characters, 236 shown); timing (253 characters, 238 shown).
What step 5 prints for it: isr-rtos: USART2_IRQHandler(), line 26 | shared-volatile: USART2_IRQHandler(), line 22 | shared-volatile: USART2_IRQHandler(), line 23 | w1c: USART2_IRQHandler(), line 30 | extra-ring-full: USART2_IRQHandler(), line 23
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./cortexkit.js"); require("./cortexreply.js")), then:
const K = CortexKit, R = CortexReply;
const f = K.form(Object.assign(K.blank(), { label: "UART RX", language: "c", core: "m4f", platform: "stm32f4", nvic_bits: "4",
code: "void USART2_IRQHandler(void) { HAL_Delay(1); }" }));
// f.rows (area, id, status, evidence, where, cite), f.isrs, f.nvic, f.timing.lines, f.verdict
// K.lex(code, "c") - comments and literals blanked, line numbers kept
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 flagged, 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://cortex-check.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://cortex-check.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"]
// cortex-check.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://cortex-check.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://cortex-check.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
}
// CortexCheck.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 CortexCheck {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://cortex-check.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://cortex-check.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://cortex-check.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://cortex-check.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":"cortex-check"}' | 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": "cortex-check"})["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: "cortex-check" })).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": "cortex-check"}, 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", "cortex-check"), 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: "cortex-check" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "cortex-check"])["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 = "cortex-check" }))!["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", "module": " 1 | // the module, numbered ...", "context": "Module: ... | Core: ... | Timing budget ...", "carry": "", "facts": "Page checks ... | must: ..."}
# - 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", "module": " 1 | // the module, numbered ...", "context": "Module: ... | Core: ... | Timing budget ...", "carry": "", "facts": "Page checks ... | must: ..."}
# - 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", module, context, 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 cortex-check:<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, priorities, timing, hardware_tests and caveats; a rewrite reply carries filename, language, code, changes, assumptions, test_plan and caveats. The code below prints the gist of either: each finding with its place, or the file name and each change.
KEY="cortex-check:$(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 (.filename, (.changes[] | "\(.id): \(.what)"))
else (.findings[] | "\(.id): \(.where)") end' reply.json
import hashlib, time
key = "cortex-check:" + 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":
print(reply["filename"])
for x in reply["changes"]:
print(x["id"], x["what"])
else:
for x in reply["findings"]:
print(x["id"], x["where"])
import { createHash } from "node:crypto";
const key = `cortex-check:${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") { console.log(reply.filename); for (const x of reply.changes) console.log(x.id, x.what); }
else { for (const x of reply.findings) console.log(x.id, x.where); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "cortex-check:" + 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, a, b := "findings", "id", "where"
if reply["task"] == "rewrite" {
fmt.Println(reply["filename"])
key, a, b = "changes", "id", "what"
}
xs, _ := reply[key].([]any)
for _, x := range xs {
xx := x.(map[string]any)
fmt.Println(xx[a], xx[b])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "cortex-check:" + 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")) {
System.out.println(reply.path("filename").asText());
for (JsonNode x : reply.path("changes")) System.out.println(x.path("id").asText() + " " + x.path("what").asText());
} else {
for (JsonNode x : reply.path("findings")) System.out.println(x.path("id").asText() + " " + x.path("where").asText());
}
require "digest"
key = "cortex-check:#{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"
puts reply["filename"]
reply["changes"].each { |x| puts "#{x["id"]} #{x["what"]}" }
else
reply["findings"].each { |x| puts "#{x["id"]} #{x["where"]}" }
end
$key = "cortex-check:" . $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") { echo $reply["filename"], "\n"; foreach ($reply["changes"] as $x) echo $x["id"], " ", $x["what"], "\n"; }
else { foreach ($reply["findings"] as $x) echo $x["id"], " ", $x["where"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"cortex-check:{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") { Console.WriteLine(reply["filename"]); foreach (var x in reply["changes"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["what"]}"); }
else { foreach (var x in reply["findings"]!.AsArray()) Console.WriteLine($"{x!["id"]} {x["where"]}"); }
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: cortex-check:$(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", "module": " 1 | // the module, numbered ...", "context": "Module: ... | Core: ... | Timing budget ...", "carry": "", "facts": "Page checks ... | must: ..."}
# - 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": "cortex-check:" + 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": `cortex-check:${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", "cortex-check:"+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", "cortex-check:" + 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" => "cortex-check:#{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: cortex-check:" . 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", $"cortex-check:{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);