- Read before writing. Never edit a file you haven't read this session.
- Read hotpath code in full (
FrameBuilder,CircularBuffer,FrameReader,Dashboard) before touching it. ReadBluetoothLE.h/.cppbefore writing any new driver — it's the canonical reference. - Read existing signal/slot wiring in a file before adding or changing any.
- Plan before multi-file changes (>3 files): state the plan, get confirmation. Non-trivial
or multi-file work runs through spec-driven development (
/ss-spec→/ss-plan→/ss-tasks→/ss-implement); see doc/claude/spec-driven.md. - Edit, don't rewrite. Targeted
Editcalls; full rewrite only when asked or >70% changed. - No preamble, no trailing summary — except a one-line statement of intent before non-trivial work, and one or two sentences naming what changed (and what's next) when you stop. Skip both on trivial edits. (The Context Canary line below is exempt — it is mandatory on every response.)
- Do not create markdown/doc files unless asked. Share info conversationally.
- Never build — but always syntax-check. Never invoke
cmake/jomor produce build artifacts — the developer builds the app themselves. The ONE sanctioned compiler use (2026-10-04) isscripts/syntax-check.py(clang-fsyntax-onlyvia the newest compile database underbuild/): run it on every C++ file you edit, every time — lint checks shape, only the compiler checks truth (types, includes, reachability), and every AI batch-edit failure of 2026-10 was compiler-class and lint-invisible. A hook runs it per edit; a SKIP (stale database) means the claim "it compiles" is unverified — say so. - Runtime experiments are sanctioned. Ground truth beats on-paper reasoning: drive a
running app through the API server (
localhost:7777,tests/utils/api_client.py) to probe hypotheses, not only to verify fixes; runctestand already-built binaries (--selftest,--benchmark-hotpath) against an existing build dir; prototype ideas as throwaway Node/Python sims in the scratchpad. Launching the GUI app and compiling stay the user's. - Sample the running app before theorizing about a GUI stall. Never reason a freeze,
stutter, or ignored resize out of the source —
sample <pid>in the background while the user reproduces. Recipe, known stack signatures, and the 2026-08-13 incident: doc/claude/common-mistakes.md "Diagnosing a GUI Stall". - Update CLAUDE.md for any architectural change that future me would otherwise miss.
scripts/is the style contract. When in doubt, run it; don't restate it here.
End every response — including one-word answers — with this exact line, reproduced from memory:
canary: qt 6.11.2 | cpp20 | hotpath 256k (native 1024k, js 64k) | queue 65536 | api 7777 | style 100/2
It is a context-health probe in plain ASCII. This file is in context every turn, so the line is reproduced from the prompt, not recalled: the probe measures whether the always-present instruction still steers. Compaction or attention drift shows as a mutated or missing value, and which value drifted names the rule that lost its hold. Retyping it also re-anchors the constants (J-Space, below). One plain-text line at the very end.
- From the prompt only. Never Read/Grep this file or anything else to reconstruct the
line — a looked-up canary defeats the attention measurement. If you cannot reproduce it
confidently, write
canary: lostinstead of guessing: that is the signal firing. - Verbatim. Same values, same order, every turn. Do not paraphrase, reformat, extend, or "improve" it.
- For the developer: a mutated, partial, or vanished canary means a degraded context:
treat recent output as suspect, checkpoint,
/compactor restart.
Predictability, not productivity — the difference between a tool the user re-audits every time and a collaborator they rely on. Capability without predictability gets disabled. Full text and the incidents behind each rule: doc/claude/trust-contract.md.
- Never touch, revert, or restore files outside your own edits — the one rule whose
violation loses real work. A working-tree file you did not edit this session is the
user's in-progress work: never
git checkout/restore/reset/stash/cleanit, overwrite it, or "clean it up". Derived artifacts (.ts/.qm, build output) included. In the way or seems wrong? Quote the path in chat and ask. When unsure whether a file is yours: it is not. - Stay in your lane. Spot an adjacent fix? Name it in chat ("noticed X — want it in this pass?") rather than slipping it into the diff.
- Show the why, not the what. Comment/reply/commit message explains why, and only when the choice was non-obvious. One sentence.
- State the plan before non-trivial work — visible before execution, not a summary
after. Operationalized as
/ss-spec, whose premise check measures the one number that would kill the spec. Spec and plan land in their own commit before the code;scripts/commit-shape.pyrefuses that mix, a protected surface staged with other code, and bulk without aMechanical:trailer. Never cite the archive as proof of gating. - Self-review before handoff. Re-read the diff: is this what was asked, and only that?
- Security-sensitive surfaces are decision-visible.
app/src/Licensing/,core/Core/Licensing/,MachineID, and the API Server auth/consent files: any behavioral change there is named in chat with its failure modes BEFORE it lands, and never rides inside a bulk/agent package (the 2026-09-03 stored-fingerprint inversion shipped a seat-cloning hole into master for a month exactly that way). The checklist is doc/claude/specs/0042-license-token-hardening/consumers.md.
Quality concentrates in planning and review; bulk token generation doesn't need the strong model. Anthropic's advisor-tool measurements and the spec-0069 post-mortem agree on the shape: the main session advises, cheaper executors type.
- The main session plans, reviews, and integrates; mechanical execution is delegated. Applying an approved brief, repetitive multi-file sweeps, test-writing against a fixed pattern: run them in subagents with a cheaper model override (Sonnet by default, Haiku for purely mechanical work). Design decisions, hotpath edits, and anything on a protected surface stay in the main session.
- Two high-tier touchpoints per task, not continuous involvement. One after
orientation but before committing to an approach (the
/ss-plangate), one before declaring done (the self-review gate). Between them the strong model directs; it doesn't type. - Briefs and reports are budgeted. A per-component brief fits half a page: files, the invariants that bind, one acceptance check. Subagents return conclusions, never file dumps. Measured result: hard output caps cut advice volume ~7x with no detectable quality loss — unbounded high-tier prose is the dominant avoidable cost, so cap first and raise only on evidence.
- Searches fan out, conclusions come back. Multi-file exploration runs in read-only search subagents; the main context receives the answer, not the excerpts. This also keeps the cached prompt prefix stable.
- No process tax on trivial work. Mandating a consult/plan step on tasks whose first action needs no planning measured net-zero quality gain at pure cost. Keep the "skip for trivial" escape hatches, and never write a rule that both mandates and restrains the same mechanism — conflicting steering is worse than either alone.
All scripts in scripts/ are CWD-independent and write LF endings; run from anywhere. The
per-script table lives in doc/claude/scripts.md. What binds you:
run sanitize-commit.py before every commit (sanitize only — it never commits or
pushes); code-verify.py is the structural linter (--fix rewrites, --check regenerates
.code-report; errors block CI). Suppression: // code-verify off / // code-verify on
(C++/QML), <!-- doc-verify off --> / <!-- doc-verify on --> (Markdown) — suppressions
are a code-review trigger. .code-report / .doc-report / .claim-report / .tidy-report are the cleanup
checklists; advisories are baseline debt, new code still clears them.
term-verify.py gates user-facing terminology (spec 0100): one name per concept across UI
strings, command manifests, the manual and the assistant corpus, banned synonyms in
scripts/term-rules.json; internal identifiers keep their old names on purpose.
Four gates ratchet growth against a checked-in baseline instead of capping absolute size,
and --accept re-seeds each: code-verify.py --singleton-census, code-verify.py --tu-census (excess over 1500 lines), claim-verify.py, which resolves every path,
symbol and pinned constant in the AI-facing docs — the canary's values included — against the
tree, and ledger-verify.py (uncodified ledger rows); layer-verify.py gates the core/
layering the same way. Baselines, fences and anchor mechanics:
doc/claude/scripts.md.
Python/pytest suite under tests/. Full catalog — per-file coverage, fixtures, markers, the
delay/operation-mode tables, the C++ ctest tier, the --selftest in-app tier —
lives in tests/README.md; read it before writing a test. What binds you:
- You don't build or launch the app, but you may drive a running one. Integration,
security, and performance tests drive a running Serial Studio over TCP — they need the app
up with Preferences → API & Plugins → Enable API Server (
localhost:7777). The user launches it; once up, you may run those tests and poke the API directly (tests/utils/api_client.py) to test hypotheses. Probe first:nc -z 127.0.0.1 7777. tests/scripts/is the exception you can run — pure JS frame-parser unit tests, fresh Node.js subprocess per case, no Qt, no app.pip install -r tests/requirements.txtonce;pytest.iniregisters all markers and a 30 s per-test timeout.scripts/tests/is the tooling tier, also yours: linter fixtures, census gates, the CI shape, the hooks on fixture transcripts.- C++ units under
app/tests/(spec 0032) run viactest(no CMake presets — the repo uses hand-writtencmake -B ... -D...configures; CI inlines the unit-tier flags) — the maintainer builds; you may runctestagainst an existing build dir (never configure or compile).--selftestsuites run insideCLI::process()before the composition root: never touch an application singleton there. - The C++ hotpath has no pytest path — throughput is the user-run
--benchmark-hotpathgate (see Threading & Hotpath), piece correctness the ctest tier;ci.ymlruns both.
pytest tests/scripts/ -v # JS-parser units (Node.js only) — safe for you
pytest tests/integration/ -v # all integration (needs running app)
pytest tests/ -m "not destructive" -v # skip server-crashing testsSerial Studio: cross-platform telemetry dashboard, Qt 6.11.2 + C++20. Data sources: UART,
TCP/UDP/WebSocket/HTTP (one Network driver, spec 0068), BLE, Audio, Modbus, CAN Bus (Qt
plugins plus direct gs_usb/SLCAN/Seeed backends; J1939 TP + ISO-TP reassembly, spec 0073),
MQTT (Sparkplug B both directions, specs 0073/0074: host-application subscribe with project
generation, edge-node publish with stable aliases), OPC UA (embedded open62541 + mbedTLS,
six security policies None through Aes256_Sha256_RsaPss, spec 0067), Siemens S7comm
(S7-300/400/1200/1500, in-house stack), EtherNet/IP (libplctag), IEC 60870-5-104 (in-house
stack; all three PLC/telecontrol drivers are read-only clients, spec 0073), USB (libusb),
HID (hidapi), Process I/O. 15+ visualization
widgets, 5 output (control) widgets, 256 kHz+ data rate (CI-gated; see below).
Frame parsers in JavaScript (QJSEngine), Lua (embedded LuaJIT 2.1, 5.1 + shims; per-project
Safe/Fast execution mode — spec 0051), or Built-In ("Native"
in all internal identifiers — SerialStudio::Native, CFrameParser, NativeTemplate; only
user-facing strings/docs say Built-In. Parametrized C++ templates configured via a JSON
descriptor, no user code). Per-dataset value transforms in JS or Lua, with one shared library per
language per project and per-dataset params (spec 0083). Pro features: Output
widgets, Modbus, CAN Bus, OPC UA, S7comm, EtherNet/IP, IEC 60870-5-104, Sparkplug B, MDF4,
3D, ImageView, Waterfall, file-transfer protocols (X/Y/ZMODEM), Modbus map importer,
Historian (per-session SQLite recording; "Session Database" pre-2026-08), InfluxDB 2.x sink
(core/Storage/InfluxDB/, line protocol per published block).
Aural alerts (spec 0087, every build): an ISA-18.1 annunciator (UI::Alarms::AlarmAnnunciator,
root-owned, Cpp_UI_Alarms, alarms.* API) with IEC 60601-1-8 WAV signatures played by
IO::Audio::SoundPlayer on a raw miniaudio device.
User-facing renames of 2026-08-19 — internal identifiers unchanged: Historian (was Session
Database; Sessions:: namespace, sessions.* API, "Session Databases" folder stay), Variables
(was Shared Memory; registers → variables in UI/docs, RegisterDef/registers JSON stay),
Computed Dataset (was Virtual Dataset; virtual_ field + "Virtual" JSON key stay), Canvas
Widget (was Painter; "painter" widget key, project.painter.* API, Painter* classes stay).
In doc/help the hotpath is called the "acquisition pipeline" — code, benchmarks, and internal
docs keep "hotpath". The code base is seven static libraries under core/, every one a
STRICT layer since spec 0077 (SerialStudioCore and SerialStudioProtocols with prefixed
includes; SerialStudioPipeline, SerialStudioDevices, SerialStudioStorage, SerialStudioApi
and SerialStudioUi keeping their relative include roots) plus the composition root in
app/src; a library includes and links only the layers below it. Cross-library talk goes over
Core::Bus::MessageBus (typed topics, shared-pointer messages, retained state), through the
interfaces the root binds, or through the three root-bound module sets (Core::services(),
DataModel::pipelineModules(), API::handlerContext()) — never an instance() reach into
another library; scripts/layer-verify.py fails on any include, include root or link outside
the graph, and code-verify.py --singleton-census fails on any cross-library reach.
Deep subsystem detail and the silent-breakage lookup live in doc/claude/; read the
section a pointer names before working there. The inline blocks are pointers, not substitutes.
Deliberate reasoning runs on a small set of verbalized concepts; familiar-shaped work runs on autopilot and bypasses it (doc/claude/j-space.md). The repo's rules only steer an edit if named at the point of action, so:
- Name before acting. Before any edit on a protected path (hotpath, ctor closure, signal wiring, cmake flag modules), state in chat the 3-5 invariants that bind this change — in your own words, not a doc citation.
- Few, late, specific. Only the constraints that bind the change at hand; never recite whole rule files.
- Counterfactual check at handoff. Which rule does this diff most risk violating, and what concrete evidence says it doesn't? Name both.
- Diverge by naming. Design/review work sketches named alternatives before recommending (the human still gets one recommendation, per working-relationship.md). At least one alternative must violate an assumed constraint — then check whether that constraint actually binds: wrong answers usually hide in premises, not derivations. Wide design spaces may fan out a judge-panel workflow of independent attempts.
- Externalize long state. Write intermediate state into durable artifacts (spec/plan/ tasks files, a chat checklist); re-name only what binds the current edit.
The rules most likely to cause silent breakage. Full detail (data flow, threading table,
cached flags, benchmark mechanics) in
doc/claude/architecture/dataflow.md; invoke the
ss-hotpath skill before editing these paths (nothing loads it for you) and it re-states them,
including the spec-0055 block caps, the time-ring/plot-clock rules, and the kernel macros.
- The frame pipeline runs on
IO::PipelineHost's processing thread (spec 0051 M3), not the GUI thread. FrameReaders,FrameParserandFrameBuilderall live there; the GUI drains finished pooled frames from an SPSC ring inDashboard::onDisplayTick. Public FrameBuilder/FrameParser mutators SELF-MARSHAL — GUI→pipeline waits go throughIO::PipelineHost::runOnObjectThread(event-loop-backed), pipeline→GUI reads throughrunOnGuiThreadBlocking. Never add a plainBlockingQueuedConnectionfrom the GUI into the pipeline. runOnObjectThreadis for command-rate waits, never for a GUI-thread script API. The wait spins a nestedQEventLoopthat fires the display tick and re-enters the calling script.DataModel::readTableView/writeTableStore(DataTable.h) hold the ONE routing rule, and both the JS bridge and the Lua closures go through them. Nolua_*call may run inside a routed lambda. Any new GUI-callable script API follows the same rule — see doc/claude/architecture/scripting.md "Data Tables".FrameReaderandCircularBufferare pipeline-thread / SPSC. Never add mutexes. Recreate viaresetFrameReader()/reconfigure(). Enforced: code-verify:spsc-lock.- In-pipeline signal hops must be
Qt::DirectConnection. Queued between two pipeline-thread objects fills the 65536-slot queue at 10+ kHz and drops frames. GUI↔pipeline traffic is chunk/command/tick rate only — never a per-frame queued emission. Enforced: code-verify:pipeline-queued-hop. - No allocation on the publish path. Blocks come from a pool (
use_count()==1probe, aliasing shared_ptr) with columns sized once at bind —DataModel::BlockStagerowns it on the frame lane,StreamProcessor::claimBlockSlot()on the stream lane. The one copy isclone_block_trimmed, made byDataModel::BlockPublisherfor the async sinks — deliberate and gated on a sink being on. - One publication payload, one ingestion path (spec 0055). Nothing publishes a
TimestampedFrameany more:FrameBuilder'sBlockStagerstages parsed rows into a pooledDataModel::DataBlockand flushes on the display tick or a sample cap (kFrameBlockSampleCap64,kStreamBlockSampleCap4096);BlockPublisherfans the finished block out. Dense sources do per-sample work on their ownIO::StreamWorkerthread but emitblockReadyqueued to the pipeline thread, the SINGLE producer for every sink. Structure travels separately as aStructureSnapshot. Never add a rate cap or a per-view reduction; an overrun drops whole blocks and counts them. - The two republish lanes never share one "already republished" mark (spec 0064), or the dashboard stays live while every recording comes out empty. Enforced: compile:DataModel::RepublishLane, ctest:tst_republish_lanes. Detail: dataflow.md "The Two Republish Lanes".
- The session boundary is the sink contract (spec 0075). Recording sinks close on
FrameBuilder::sessionBoundary(connected, paused)— pause included — and the builder flushes every open block BEFORE emitting, so the tail lands in the file that was open. The export-time tie-break is per source (monotonicSourceNs) and a uniform-grid block never takes it. - Time rings are sized from a rate, never a sample count, and a ring's clock never rewinds.
Since spec 0097 they start rate-true and grow per block on coverage (
EnvelopeRing::growFor, the one named allocation on the ingest path, never inside a per-sample loop), keep every sample up to the per-curve ceiling or the 512 MB rail, and display storage is never pinned. Enforced: compile:DSP::RingCapacity, compile:UI::PlotClockState, ctest:tst_time_ring_sizing, ctest:tst_ring_exactness, code-verify:lock-memory-scope. Rules + the 2026-08 incidents: dashboard.md "Time-Ring Sizing & the Plot Clocks". - Native + PlainText parses through the span fast lane (
trySpanLane→parseUtf8Spans→applyDatasetValuesSpans), zero steady-state allocation. Hotpath flags areDataModel::Cached<T>re-derived at 1 Hz (spec 0095); a new input wires its change signal to the cache refresh, a new cache is never a plainbool. Enforced: ctest:tst_cached_flags, anchor:cached-hotpath-flags. Read dataflow.md "Cached Hotpath Flags" before touching any. - Source owns time. Stamp at the driver boundary; never re-stamp in export/report
workers (use
monotonicFrameNs(...)as the safety net only). Enforced: code-verify:--clock-census. - Driver opens are synchronous calls; most drivers dial asynchronously behind them. An async
dial's verdict has ONE owner,
HAL_Driver::openFinished(ok, reason), emitted exactly once per attempt; a driver that reports only success wedges the connect button. Doctrine: doc/claude/architecture/io.md "Opening a Link". - Diagnostics are pulled, never pushed (specs 0033/0035). Pipeline counters are plain
quint64increments polled on the 1 Hz tick, never a per-frame signal, allocation or lock; a recreatedFrameReaderzeroes them, so consumers work on deltas. Detail: dataflow.md "Diagnostic Counters" (the polled-PLC atomic deviation: io.md). - JS scripts: always
JsScriptEngine::guardedCall(), neverparseFunction.call().setInterrupted(true)only inJsWatchdogThread.cpp. Enforced: code-verify:js-raw-call, code-verify:js-interrupt-site. - 256 kHz is a CI gate, not a slogan.
--benchmark-hotpathruns nine gates tiered off--min-fps(default 256000) as a hard gate inci.yml; don't regress it. Enforced: anchor:hotpath-min-fps. Tier table and the ~70-80%datasets+publishshare:ss-hotpathskill. - The message bus is not a hotpath primitive (spec 0076): frames and blocks keep the pooled SPSC path. Enforced: code-verify:bus-on-hotpath. Detail: directory-map.md "Message bus".
- Reuse the kernels; never inline intrinsics or invent a macro —
core/Core/DSPSimd.h(bit-exact per lane, runtime-dispatched) andcore/Core/HotpathOptimization.h; never fast-math, no-unwind or GCCoptimize("..."), and no TU is compiled wide. Rules: doc/claude/architecture/kernels.md. Debt: no intrinsic-header lint yet.
Full contract, including the ctor-edge proof and the licensing consumer inventory: doc/claude/architecture/startup.md.
ModuleManager::instantiateCoreModules()pins singleton construction order (ProjectModel before AppState, Dashboard last); never reorder or add entries without re-running the ctor-edge proof in doc/claude/specs/0001-composition-root/. Enforced: anchor:composition-root-order.- No new function-static singleton reach in a module ctor closure — the one residual hazard now that an early reach into an adopted module fails by name. Enforced: compile:Core::ModuleConstruction, script:scripts/code-verify.py#--singleton-census.
SessionContext(spec 0039) owns the nine core modules; never callSessionContext::current()from a method body, and no library includesSessionContext.h. Enforced: code-verify:arch-session-adopt-site, script:scripts/code-verify.py#--singleton-census.- Every composition root goes through
ModuleManager::composeSession(BindMode); an unbound root publishes through a null host and a sink left off its list records a valid-looking empty file. Enforced: compile:Misc::ModuleManager::composeSession. - License-gated state must exist before
restoreLastProject()or re-derive onactivatedChanged. The licensing block is the FIRST thinginstantiateCoreModules()builds after Translator (spec 0042).activatedChangedfires only on real token-validity transitions. Anything bakingSerialStudio::activated()into derived state at load time still needs aLemonSqueezy::activatedChangedhook, or late/async activation ships fallback widgets (2026-07-09: Plot3D degraded to MultiPlot).
Each row is a trigger, not a summary: read the named section before editing that area. The hazard column names what breaks silently; the doc holds the rule.
scripts/code-verify.py is the contract — read its --check output, don't re-derive the
rules. Full spec and the NASA Power of Ten live in
doc/claude/code-style.md. The handful you need before typing:
- Format: 100-col, 2-space indent, LF, pointer/ref binds to type (
int* p). No braces on single-statement bodies; blank line after a brace-free body. Max 3 nesting levels (guard clauses); functions 40-80 lines, hard limit 100; translation units under 1500 lines (cxx-tu-too-long— the function cap alone let god TUs accrete a method per feature). Runclang-format. - Headers (.h):
Q_OBJECT→Q_PROPERTY→signals:→ ctor/deleted copy →public:(instance()first) →public slots:→private slots:→private:, Christmas-tree in each block.[[nodiscard]]on every non-void return. NeverQ_INVOKABLE void(usepublic slots:). No in-header member init — ctor init list only. - Signals:
Q_EMITnotemit; lowercasesignals:/public slots:; neverSIGNAL()/SLOT(). Neverdisconnect(nullptr)as the slot — capture theConnection. - Comments: code is the spec; label, don't narrate. No comments inside a function body
(
cxx-inbody-comment, advisory) — functions are short, so the one-line/** @brief ... */above the function plus self-explanatory code carry it; fold a load-bearing why into the@brief, or fence a genuinely-needed note with// code-verify off.//---concern-group banners live between functions, never inside one. No inline EOL comments, no AI narration. Don't fake the em-dash with--— rewrite the sentence. - Naming:
CamelCasetypes,camelCasefunctions,lower_caselocals + public members,s_/m_/k/UPPER_CASEfor static/private/constexpr/macro (full table in the sub-doc). - Safety-critical (NASA Power of Ten) — hotpath violations are blockers. The ones that
bite: no alloc/Frame-copy on the dashboard path; fixed loop bounds + capped recursion;
assertion density ≥2/function;
[[nodiscard]]+ return checks at every system boundary; zero warnings; noreinterpret_cast/dynamic_caston the hotpath; SPDX header per file — first-party isGPL-3.0-or-later(relicensed from-only, 2026-07). The repo is REUSE-compliant:REUSE.toml+LICENSES/,reuse lintgates in CI. - Assertions are
SS_ASSERT(cond, action)(core/Core/SSAssert.h), notQ_ASSERT: the condition evaluates in every build; debug aborts, release reports once per site and runs the recoveryactioninstead of the guarded code. Condition side-effect-free and cheap; action side-effect-complete, single statement, no top-level comma, nevercontinue/break(useSS_ASSERT_LOG(cond); if (!(cond)) continue;). On the per-frame/per-cell kernels useSS_ASSERT_HOTPATH(cond)instead — see doc/claude/architecture/kernels.md.
