Files
Trevin Chow 4e0e8f845a feat(cli): add BLE device-sniff and device-spec CLI generation (#2601)
* feat(cli): add BLE device spec model

* feat(cli): add BLE evidence analyzer

* feat(cli): add BLE device-sniff command

* test(cli): add BLE device-sniff golden

* feat(cli): add BLE replay adapter

* feat(cli): add BLE replay probe

* feat(cli): add live BLE probe adapter

* feat(cli): classify BLE command guidance

* feat(cli): add BLE session mode tiers

* feat(cli): generate minimal BLE device CLI

* feat(cli): generate replay BLE controls

* feat(cli): generate BLE session scaffold

* feat(cli): generate BLE telemetry store

* feat(cli): show BLE device capabilities

* feat(cli): verify BLE device commands safely

* feat(cli): package live BLE probe testing

* feat(cli): integrate BLE device sniff docs

* refactor(cli): simplify BLE device sniff code

* fix(cli): harden BLE probe evidence workflow

* fix(cli): connect BLE inspect after Linux scan

* feat(cli): expose BLE probe in device sniff

* fix(cli): harden BLE probe evidence and live adapter

Code-review fixes for the BLE device-sniff path:

- analyze: degrade gracefully instead of aborting the whole analysis when
  evidence omits the optional safety field, has a non-sluggable command
  name, or references an undiscovered characteristic
- redact: fix possessive-term replacement order (no more "redacted's"
  residue) and apply operator redaction terms to name/display-name fields
- bleprobe: carry redaction terms through scan/inspect/read/subscribe
  output so merge can act on them
- ble-probe: cancel live scans/subscriptions on SIGINT/SIGTERM
- live adapter: make service/characteristic discovery context-cancellable
  so an unresponsive device no longer hangs, classify
  permission/not-found/disconnected errors, pass context errors through
  verbatim, and stop holding the notification mutex during hex encoding
- device-sniff: relabel archived evidence so it no longer implies captured
  values are scrubbed (only device names are redacted)
- replay adapter: cache device candidates instead of recomputing per call

Adds redact, analyze graceful-degradation, and live-adapter helper tests.

* refactor(cli): make BLE live adapter testable via a driver seam

Split the tinygo-backed live adapter so its lifecycle and concurrency logic
no longer depends on the BLE backend type, turning the largest untested
surface of the BLE probe into ordinary testable Go:

- driver.go: bleDriver / bleDevice / bleService / bleCharacteristic
  interfaces
- adapter_live.go: liveAdapter holds a bleDriver and owns all lifecycle
  logic (scan duration/drain, context cancellation, service/characteristic
  discovery, notification collection, error mapping) with no backend types,
  so it compiles and tests on every platform
- driver_tinygo.go: thin tinygo.org/x/bluetooth wrapper -- the only file
  importing the backend -- behind the existing build tag
- adapter_live_test.go: stub-driver lifecycle tests that run without
  hardware, including under the ble_replay_only build

No behavior change for valid inputs or normal error paths. Malformed-UUID
inputs now route through mapLiveError (wrapped as "unsupported") like other
backend errors instead of being returned raw; no test or workflow depended
on those exact strings.

* chore(cli): gitignore docs/brainstorms/ artifacts

* fix(cli): harden BLE live adapter lifecycle behind the driver seam

- Add bleDriver.NeedsPreScan() so the BlueZ pre-scan path is a seam decision
  exercised against the test stub instead of an inline runtime.GOOS check.
- Bound the post-StopScan drain in Scan/ensureDeviceSeen so a backend that
  ignores StopScan cannot pin a cancelled command open; keep candidates
  collected before a clean backend return.
- Report device-not-found (not a raw context deadline) when a pre-scan ends
  without the target in range.
- Add a 30s tinygo ConnectionTimeout and clamp oversized characteristic reads
  to the ATT 512-byte max.
- Store backend characteristics by pointer so EnableNotifications' pointer-
  receiver bookkeeping survives (disable is no longer a no-op).
- Cancel the root command context on SIGINT/SIGTERM for graceful shutdown of
  long-running and hardware-backed subcommands.
- Write device-spec and analysis artifacts 0600 (carry control payloads and
  device identity); curtail live scan/subscribe windows under dogfood.
- Document the seam stub-fidelity pattern in docs/PATTERNS.md + solutions/.

* fix(cli): close BLE redaction and analyzer edge cases from review

Redaction completeness (the term an operator asks to scrub must not leak
into the saved-for-sharing artifact):
- Scrub ActionMarker.Label and CommunityReference.CommandName, which are
  slugged into device-spec command names and evidence summaries.
- Match redaction terms case-insensitively so 'Owner' also catches
  'owner'/'OWNER' in advertised names.

Analyzer / spec robustness:
- Slug the (already-redacted) spec name so an arbitrary advertised device
  name can never abort Validate's IsSlug check; DisplayName keeps the prose.
- Honor an explicit session.one_shot_fallback:false for optional mode
  instead of unconditionally forcing it true (Validate permits false).
- Stop ReplayAdapter.Scan from filtering in place over its persistent
  candidates backing array, which corrupted state on adapter reuse.
- Share one NormalizeUUID (exported from devicespec) instead of duplicating
  the helper in the ble package.

All touched packages pass; golden verify is unchanged (29/29).

* refactor(cli): satisfy modernize linter in device spec

- validateEnum uses slices.Contains instead of a manual loop.
- Drop the no-op json omitempty on nested struct fields (Identity,
  Capabilities, Session, Payload); encoding/json never omits structs, so
  this changes no output. YAML keeps omitempty (yaml.v3 honors it for zero
  structs). .golangci.yml enables modernize, so these blocked the push.

* fix(cli): address BLE review feedback

- keep legacy persistent session parsing private

- skip malformed BLE correlation timestamps with report ambiguities

- reuse compiled redaction regexes across evidence fields

Note: pre-existing failure in go test ./... internal/generator govulncheck gate not addressed by this PR.

* feat(cli): add generated BLE session runtime

* fix(cli): skip malformed BLE hex evidence

* fix(cli): annotate read-only BLE device commands

* fix(cli): require confirmation for physical BLE commands

* feat(cli): store generated BLE session summaries

* fix(cli): bump Go floor for govulncheck

* fix(cli): document BLE physical-effect confirmation

* fix(cli): correct BLE alias probe path, redaction claim, and live read clamp

Address code-review findings on the device-sniff BLE branch:
- bluetooth-sniff alias now reports its own command path in probe
  doctor/smoke output instead of the nested "device-sniff ble" path.
- device-sniff ble evidence summary states what redaction actually does
  (addresses pseudonymized; names only via --redact-term) rather than
  claiming device names are always redacted.
- live adapter Read clamps a negative backend byte count so buf[:n]
  cannot panic.
- drop an incident-marker comment from a redaction test.

Adds regression tests for the alias doctor binary name and the
negative-count read clamp; updates the device-sniff-ble-sample golden
for the corrected message.

* test(cli): add BLE generated-output proof cases

* fix(cli): report BLE archive name redaction by effective term set

The device-sniff archive note keyed off the raw --redact-term slice, so it
understated redaction when terms arrived from the evidence file and over-claimed
on a blank --redact-term. Key the note on the effective (normalized, non-empty)
merged term set via a shared HasEffectiveRedactionTerms helper.

Bundles review fixes from /ce-code-review:
- redaction-note accuracy + table-driven note-state coverage
- assert the withheld/opaque SKILL.md fallback omits the dry-run/confirm clauses
- document that the typed-const RequiresPhysicalConfirmation method and the
  emitted string-literal requiresPhysicalConfirmation gate must stay in sync

* docs(skills): require BLE mapping research gate

* fix(cli): make verify, dogfood, and live-dogfood device-CLI aware

BLE device CLIs (protocol: ble) have no sync data pipeline, no
internal/client HTTP client, no agent-context command, and the device
SKILL template omitted the canonical install block. The HTTP-API-shaped
verifier checks reported false-negative failures even when every command
passed:

- verify "Data Pipeline: sync crashed" -> skip the sync->sql->search
  test for device CLIs (dimension satisfied).
- dogfood auth-protocol mismatch (no client.go), sync-generic, and an
  agent-context example-discovery crash -> skip the HTTP-only verdict and
  issue rules for device CLIs; fall back to a bounded --help command-tree
  walk when agent-context is absent.
- live dogfood crashed on the missing agent-context command -> return a
  clean "unverified-device" verdict (manual --live testing is the real
  gate for device CLIs).
- verify-skill canonical-sections failed -> the device SKILL template now
  emits the canonical "Prerequisites: Install the CLI" block via the
  shared CanonicalSkillInstallSection helper.

Adds isDeviceCLIDir (delegating to the canonical isDeviceBackedCLIDir).
Device golden SKILL fixtures updated for the new install section.

* feat(cli): emit MCP surface for BLE device CLIs and make scorecard device-aware

Generated BLE device CLIs now ship a stdio MCP server (cmd/<name>-pp-mcp)
that mirrors the Cobra tree via the API-agnostic cobratree walker and exposes
a read-only device-context tool. The MCP binary execs the companion CLI, so it
carries no BLE/CGO dependency and builds anywhere.

Make the scorecard device-aware: HTTP-shaped dimensions (local_cache, vision,
workflows, insight, agent_workflow_readiness, data_pipeline_integrity,
sync_correctness) are marked N/A for device CLIs so they drop from the
denominator instead of scoring a false 0. Add device-specific doctor and
error-handling scorers keyed on BLE reachability and safety remediation.

Single-source the cobratree walker file manifest via cobratreeWalkerTemplateFiles()
across the HTTP and device generators (previously triplicated).

* feat(cli): add novel-command hook to generated device CLIs

Generated device CLIs' root.go now declares a nil-guarded novelCommands
function variable and invokes it after wiring the generated command tree.
Hand-authored commands attach by setting the var from an operator-owned file
(naturally preserved verbatim by regenmerge as a NOVEL file), with no edit to
generated files. The default build is a no-op (nil hook).

This is the extension point for the forthcoming generator-emitted live BLE
control surface, and lets existing device CLIs (e.g. WalkingPad) stop
hand-editing the generated root.go to register their novel commands.

* feat(cli): emit BLE adapter seam into generated device CLIs

Generated device CLIs now carry a device-neutral BLE central seam in
internal/device/: bleBackend/bleLink interfaces (ble.go, always compiled), a
tinygo.org/x/bluetooth live driver behind the ble_live build tag (ble_live.go,
CGO), and a pure-Go stub for the default build (ble_stub.go). The seam addresses
GATT characteristics by UUID, so one connection serves every command and
telemetry stream the device spec declares.

The default build links no BLE stack and stays CGO-free (verify/dogfood/golden
and `go test` remain hardware-free); `go build -tags ble_live ./...` links the
real adapter. tinygo is added to the generated go.mod and retained by `go mod
tidy` via the tag-gated import (the tools.go-style mechanism), so -tags ble_live
resolves without polluting the default build.

This is the plumbing the forthcoming LiveTransport (Phase 3) drives; no command
wires it yet.

* feat(cli): wire live BLE transport, flags, and doctor into device CLIs

Generated device CLIs can now drive real hardware. internal/device/live.go
emits LiveTransport, which implements the existing Transport interface over the
BLE seam: it scans by the spec's ServiceUUIDs (now emitted in spec.go), connects,
and reads telemetry / writes command payloads by characteristic UUID. The same
status and command surface that replays evidence by default actuates the device
under --live.

root.go gains persistent --live/--address/--timeout flags and resolves the
transport at run time (deviceTransport): LiveTransport under --live, replay
otherwise (selection can't happen at construction because persistent flags are
unparsed then). A generated doctor command reports BLE-compiled state, verify/
dogfood env, service UUIDs, and — with --live — device reachability.

Safety carries over: physical-effect confirmation, dry-run preview, and a
verify-mode short-circuit in LiveTransport that returns verify-live-noop before
touching the backend, so --live never dials under PRINTING_PRESS_VERIFY. The
default build stays replay-only and CGO-free; -tags ble_live links the live path.

Tier-1 (fixed-payload writes, readable telemetry) works end to end with no
hand-authoring; notify-based/stateful protocols still need a codec (Phase 5).

* feat(cli): Tier-1 live BLE control with a fake-backend test and scan command

Make the generic live path testable and complete it end to end for fixed-payload
devices:

- internal/device/ble.go gains bleBackendFactory, a var indirection over the
  build-tag-selected newBLEBackend, so tests can inject a fake backend.
- internal/device/live_test.go (emitted) exercises the Tier-1 path — command
  payload write, dry-run no-write, RSSI-sorted scan — against a fake backend.
  No hardware, no build tag, so the printed CLI's own `go test` proves its
  generic live transport works in CI.
- A `scan` command discovers nearby devices by service UUID under --live. It is
  mcp:hidden (inherently live, non-functional through the replay-only MCP exec).

A fixed-payload device now controls fully with zero hand-authoring: scan/connect,
write captured payloads, read readable telemetry. Notify-based/stateful protocols
still need a codec (Phase 5).

* feat(cli): add Dial/Link connection API and DeviceCodec hook for Tier-2 devices

Enable hand-authored control of stateful/parameterized BLE devices without
reimplementing the BLE stack:

- Export Link (renamed from bleLink) and Dial(ctx, address, timeout) (Link, error).
  Hand-authored commands wired through the novelCommands hook open a connection
  with Dial and drive it via Link (write/read/subscribe/close) — everything
  WalkingPad's client.go did, now generated. Dial refuses under verify
  (ErrVerifyMode) as a fail-closed backstop; LiveTransport.withLink uses it.
- Add the DeviceCodec interface (DecodeTelemetry) + a codec hook var. When an
  operator registers a codec, the generated status command surfaces decoded
  telemetry values; the default (nil) reports raw hex.

The emitted live_test.go now also covers Dial's verify refusal and codec-decoded
status, all against the injected fake backend (no hardware).

This is the Tier-2 enabler: static/readable devices stay zero-hand-authoring;
stateful protocols implement only their codec + novel commands on top of the
generated Dial/Link/codec surface. Parameterized-command spec modeling is left
as future work (operators express it through novel commands today).

* docs(skills): teach generated live BLE control and the Tier-2 codec responsibility

The device-sniff-ble reference now explains the generated CLI's live surface
(replay by default; -tags ble_live + --live to actuate) and the agent's
responsibility per device class: Tier-1 (fixed-payload + readable telemetry)
works with zero hand-authoring; Tier-2 (stateful/parameterized protocols) MUST
get a hand-written codec plus novel commands built on the exported device.Dial /
device.Link / DeviceCodec surface, gated on verify/dogfood, with a codec_test.go
and a -tags ble_live build check before ship. A silently-inert Tier-2 CLI is a
failure, not an acceptable outcome.

* docs(cli): document live BLE build and flags in generated README and SKILL

Generated device README/SKILL now describe the live path: build with
-tags ble_live, actuate with --live (--address/--timeout), the doctor/scan
commands, OS Bluetooth-permission and single-client expectations, and that the
default build is replay-backed. Wording-only template change; no generator
behavior change.

* test(cli): add ble_live compile proof for generated device CLIs

The default generated build never compiles ble_live.go (the tinygo driver), so
TestGeneratedBLEDeviceLiveBuildCompiles builds the generated module with
-tags ble_live. Gated to Linux, where the BLE backend is pure-Go (D-Bus) and
compiles toolchain-free in CI; macOS (CGO/CoreBluetooth) and Windows (WinRT) are
covered manually.

* refactor(cli): dedup live CommandResult construction and tidy service-UUID set

Extract liveResult(command, transport) so LiveTransport.ExecuteCommand's three
return paths share the common CommandResult fields and set only the
distinguishing dry-run/verify fields. Use map[string]struct{} for the
service-UUID dedup set in templateData. No behavior change (emitted device tests
unchanged and passing).

* feat(cli): parameterized device commands via spec params and a codec encode hook

Device specs can now declare typed positional parameters per command
(commands[].parameters: [{name, type}]). The generator emits the parameterized
cobra command — <arg> usage, exact-arg validation, safety gating, dry-run,
verify no-op — and routes the args to the device codec.

DeviceCodec gains EncodeCommand(command, args): with a codec registered, it owns
payload construction (parameterized values, framing, checksums); without one,
fixed-payload commands write their captured bytes and a parameterized command is
a hard error rather than a silent static write. Transport.ExecuteCommand now
takes args (replay ignores them; live encodes via the codec and reports the
bytes actually sent). The emitted live_test.go covers codec encode + the
parameterized-without-codec guard against a fake backend.

This makes Tier-2 control "implement your codec, get a full CLI": operators write
only the encode/decode logic, not the command boilerplate. Stateful choreography
still uses hand-authored commands on Dial/Link.

* docs(skills): document parameterized commands and the EncodeCommand codec hook

Update the device-sniff-ble Tier-2 guidance: implement device.DeviceCodec
(EncodeCommand + DecodeTelemetry), declare parameterized commands in the spec so
the generator emits the command surface, and reserve hand-authored Dial/Link
commands for stateful choreography only.

* fix(cli): score device CLIs on device shape instead of failing them

Six Steinberger scorers keyed off HTTP-CLI structure a BLE device CLI does not
have — separate per-command files, HTTP README sections, endpoint counts, API
response types — so a healthy device CLI scored a false F (output_modes 1,
terminal_ux 0, readme 2, agent_native 3, breadth 0, type_fidelity 0). Add
device-aware variants that grade the same dimensions on the device surface:
commands in root.go, --json/--agent + text output, device README sections, the
MCP server as the agent surface, command+telemetry breadth, and the generated
typed models. type_fidelity credits typed parameters only when a command
declares them, so breadth/fidelity still scale with capability.

A minimal read-only sensor now scores ~66 (B) and a command+telemetry device
~64-70, versus ~23-28 (F) before. The HTTP scorers are unchanged.

* fix(cli): pass parameter names to the generated device command at construction

A parameterized device command rejected its own argument ("accepts 0 arg(s)").
root.go emitted Parameters into the CommandDefinitions var but constructed the
inline CommandDefinition passed to newDeviceCommandCmd without them, so the
command saw zero parameters and built cobra.ExactArgs(0). Pass Parameters in the
AddCommand call too. The generated-output test now builds the CLI and runs the
parameterized command with its positional arg end to end, which the
emission-only assertions missed.

Surfaced by retrofitting the WalkingPad CLI onto the generated surface, where
set-speed <kmh> would not accept its value.

* refactor(cli): tidy device scorers (shared cli-content helper, unrolled fidelity)

scoreDoctorDevice now reads the cli package via deviceCLIContent like the other
device scorers, and scoreTypeFidelityDevice uses an explicit if-chain (matching
the file's other scorers) instead of a []bool loop. No behavior change.

* fix(cli): make generated BLE device control reliable and MCP-honest

Hardware-dogfooding the generated WalkingPad CLI surfaced machine-level issues in the BLE device generator, fixed here and grounded in ph4r05/ph4-walkingpad's reference protocol:

- liveLink.Write now prefers an acknowledged write (write-with-response), falling back to write-without-response only when the characteristic requires it. Fire-and-forget writes were dropped on an immediate Close, silently losing control commands like stop.

- liveBackend.Scan caps its window to the caller deadline and stops on the first matching device, so a long stream deadline no longer starves the connect that follows discovery.

- Status() gains an optional telemetrySnapshot hook: notify-only telemetry (which cannot be GATT-read) captures one notification frame and decodes every field from it, instead of reading back stale/echoed bytes.

- Held-connection devices (session.mode: required) hide their one-shot mutating control commands from MCP. A one-shot tool cannot drive a device that needs a sustained connection, so agents get reads plus the operator's held-connection command rather than a write tool that cannot actuate.

Generated-output tests cover acknowledged-write preference, snapshot decode + error propagation, and session-gated MCP hiding. Golden fixtures updated for generate-device-ble.

* feat(cli): device-spec transport contract and behavioral quirks

Add a first-class operational protocol contract to the device spec so the facts an agent synthesizes from references are captured once and consumed by the machine, rather than rediscovered command-by-command on hardware.

Schema (internal/devicespec): transport: write_mode, command_spacing_ms, connect_ceremony, settle_delays, poll_cadence_ms, teardown, single_client — the quantitative 'how to talk to the device' contract. quirks: cited {category, summary, handling} behavioral facts that do not reduce to a field (init tricks, stale-session gotchas, firmware opcode shifts) — the qualitative half.

Generator consumes it: command_spacing_ms emits a paced liveLink.Write (sleeps the deficit before every write — no hand-authored pacing wrapper); write_mode selects the write path (acknowledged default vs without-response); doctor surfaces the contract and quirks (text + JSON) at runtime.

Skill + docs: the device-sniff-ble research gate becomes a contract-synthesis gate — expand from seeds, extract both halves, do not relearn cited facts, and dogfood to verify the contract rather than discover it.

Generated-output tests cover paced-writer/write_mode/quirks emission; the session golden fixture exercises the paced writer under -tags ble_live. Golden updated for the device cases (doctor now reports the contract).

* feat(cli): device-spec workflows and workflow-fidelity QA gate

Add a workflows: element to the device spec: named, ordered, cited operating
sequences (the proven "spine" for each user goal) that compose the transport:
facts and the capabilities action map. Parsed + validated and surfaced in the
generated doctor (text + JSON) like quirks; it does not drive codegen — it is
the contract the implemented control flow is checked against in the QA
workflow-fidelity pass and confirmed during dogfood.

Also harden the device-sniff-ble research gate: require an explicit external
web-research pass beyond user-provided seeds, a recorded research ledger, and
>=2 independent sources corroborating the contract.

Atomic command/transport facts alone proved insufficient: without the proven
end-to-end sequence written down, a from-scratch implementation rediscovers it
by guessing on hardware. Capturing the spine and diffing the implementation
against it replaces guessing with a checkable contract.

* fix(cli): surface workflow notes and tidy empty-goal output in device doctor

Follow-up polish on the device workflows feature: doctor now emits each
workflow's notes in --json (the field carried the per-workflow gotcha but was
never rendered on its only surface), and the text output drops the dangling
"name:  (N steps)" colon when a workflow has no goal. Adds notes/goal test
assertions.

* fix(cli): give device CLIs a version command via a shared template

Device CLIs were generated with no `version` command and no `--version` flag:
the device generator forked its own root template and dropped the version
command the HTTP path emits, so every device CLI failed the `version` quality
gate and could not be published.

Extract the version command into a shared internal/cli/version.go (version.go.tmpl)
emitted by BOTH the HTTP and device generators, replacing the inline copy that
lived only in the HTTP root template. HTTP behavior is unchanged (version still
works, now single-sourced); device CLIs now carry the version subcommand and the
--version flag like every other printed CLI.

This removes a copy-paste divergence the fork introduced — it copied
API-agnostic boilerplate and let it drift. version is the first shared scaffold
file beyond the cliutil/cobratree helpers both generators already share.

* fix(cli): emit AGENTS.md, LICENSE, NOTICE, .goreleaser.yaml for device CLIs

The device generator forked its own file manifest and dropped the four standard
publish artifacts the HTTP generator emits, so every device CLI failed the public
library's publish-completeness gate (same fork-and-drop as the version command).

Share LICENSE/NOTICE/.goreleaser.yaml templates with the device generator
(extending deviceTemplateData with Owner/CompactDescription/ProseName/VisionSet and
adding modulePath + the extracted yamlDoubleQuoted helper to its funcmap), and add a
device-aware AGENTS.md variant (agents_device.md.tmpl) that documents BLE/replay
concepts and the codec/novelCommands customization model instead of HTTP auth/sync.
HTTP output is byte-identical (the yamlDoubleQuoted extraction preserves it).

* fix(cli): exclude downloaded third-party sources from published manuscripts

A research sources/ directory holds reference repos cloned to study a protocol
(common for device CLIs reverse-engineering a wire format). publish package copied
the whole manuscript tree, so those third-party repo copies were on a path to ship
into the public library — a licensing problem and a secret/PII vector. It surfaced
only by luck: a placeholder email in a vendored README tripped the PII scan;
without it a third-party repo would have shipped silently.

- publish: shouldSkipPublishableManuscriptFile prunes any sources/ subtree from
  shipped manuscripts (machine backstop), covered by a test.
- device-sniff-ble skill: cite reference repos by URL/commit and clone them to
  scratch outside the manuscript tree, never into research/sources/.
- docs/ARTIFACTS.md: manuscripts hold authored synthesis, not third-party inputs.

* fix(cli): address BLE review feedback

* test(cli): refresh BLE skill install goldens

* fix(cli): quote device display names in Go templates
2026-06-05 03:46:49 +00:00

4.2 KiB

Local Artifacts and Public Library

Generated artifacts live under the user's home directory, not in this repo.

Local artifacts

  • ~/printing-press/library/<api-slug>/ — local library: printed CLIs the generator has produced. Directory names are keyed by API slug, not CLI name. The binary inside is still <api-slug>-pp-cli.
  • ~/printing-press/library/<api-slug>/.manuscripts/<run-id>/ — per-run manuscripts (proofs, research, discovery) embedded inside the printed CLI by printing-press lock promote. Mirrors what publish package later copies into a packaged tarball, so publish validate can find the Phase 5 acceptance marker on a freshly promoted CLI without manual cp -r from the runstate.
  • ~/printing-press/library/<api-slug>/device-spec.yaml — archived device-native spec for generated physical-device CLIs, when the source was device-sniff ble or another Device Sniff backend.
  • ~/printing-press/manuscripts/<api-slug>/ — archived research and verification proofs, keyed by API slug. One API can have multiple runs.
  • ~/printing-press/.runstate/<scope>/ — mutable per-workspace state such as current run and sync cursors.

The API slug is derived by the generator from the spec title (cleanSpecName), not manually chosen. The CLI binary name is <api-slug>-pp-cli. Never hardcode an API slug when the generator can derive it; names with periods normalize differently than you'd guess.

Manuscripts hold authored synthesis, not third-party inputs. A shippable manuscript is the research brief, absorb-manifest, proofs, and discovery captures — what the run produced. Cloning a reference library to study a protocol (common for device CLIs, which reverse-engineer a wire format from a working implementation) is research input: cite it by URL and commit, do not copy the repo into manuscripts/<slug>/research/sources/. Publishing copies of someone else's code is a licensing problem and a secret/PII vector. publish package drops any sources/ directory from shipped manuscripts as a machine backstop (shouldSkipPublishableManuscriptFile), but the research flow should keep downloaded references in scratch outside the manuscript tree in the first place.

The -pp- infix exists to avoid colliding with official CLIs. The binary notion-pp-cli can coexist with whatever notion-cli the vendor ships. The library directory is just notion/; the -pp-cli suffix appears on binary names, not directory names.

Public library

The public library is the GitHub repo mvanhorn/printing-press-library — a curated, category-organized catalog of finished printed CLIs. Users install printed CLIs from there.

Local-to-public flow: a successfully generated printed CLI lives in the local library first. The /printing-press-publish skill packages a local CLI and opens a PR against the public library repo. Merging that PR is what moves the CLI from "works on this machine" to "users can install it."

The local library and public library can diverge in two ways:

  • Expected divergence. Some files are intentionally rewritten by the publish step, most notably go.mod's module path. The polish skill's divergence check exempts these.
  • Unexpected divergence. Local edits since the last publish, such as polish in progress, manual fixes, or mcp-sync regen, that have not been pushed. The polish skill's divergence check surfaces these so you can decide whether to republish or discard the local changes.

Treat the public library as the durable artifact and the local library as the working copy. When users hit a bug, they are hitting the public library's version, not whatever is currently in ~/printing-press/library/.

Discovery archives

Discovery methods write evidence under the run's discovery archive before generation consumes the result.

  • Browser Sniff archives traffic analysis and replayable HTTP evidence.
  • Crowd Sniff archives community-source findings and wrapper-library evidence.
  • Device Sniff archives device specs, BLE analysis reports, and redacted BLE evidence. Raw stable device identifiers and executable control payloads are sensitive; redacted evidence is the default archive shape, and raw evidence retention is opt-in.