
Printing Press
- 5.2k installs
- 4.3k repo stars
- Updated July 23, 2026
- mvanhorn/cli-printing-press
printing-press is an agent skill for Agent guidance for printing-press workflows.
About
printing-press encodes the CLI Printing Press novel-features absorb rubric for scoring candidate commands before code generation. It applies kill checks for LLM-only features, external services, missing auth, scope creep, poor verifiability, and fake API reimplementation, then scores survivors on domain fit, user pain, build feasibility, and research backing. Survivors need a one-sentence buildability proof naming a real API endpoint or local data source. Output uses a transcendence table with feature, command, score, buildability tag spec-emits or hand-code, evidence citations, and optional Long Description for sibling disambiguation. Reprint rules reload prior research.json to re-score shipped versus planned novel features. See SKILL.md for setup, examples, and guardrails before production use. See SKILL.md for setup, examples, and guardrails before production use. See SKILL.md for setup, examples, and guardrails before production use.
- **Auth the user doesn't have**
- **Build Feasibility**
- Documents printing-press workflows with agent-ready commands and prerequisites from SKILL.md.
- Documents printing-press workflows with agent-ready commands and prerequisites from SKILL.md.
- Documents printing-press workflows with agent-ready commands and prerequisites from SKILL.md.
Printing Press by the numbers
- 5,240 all-time installs (skills.sh)
- +299 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #267 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
printing-press capabilities & compatibility
- Capabilities
- **auth the user doesn't have** · **build feasibility** · documents printing press workflows with agent re
What printing-press says it does
**When to read:** This file is the scoring rubric for the novel-features
subagent (Phase 1.5 Step 1.5c.5). It defines kill/keep checks, the
4-dimension score, the transcendence table format, and reprint verdict
npx skills add https://github.com/mvanhorn/cli-printing-press --skill printing-pressAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5.2k |
|---|---|
| repo stars | ★ 4.3k |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 23, 2026 |
| Repository | mvanhorn/cli-printing-press ↗ |
How do I run printing-press tasks with correct setup and documented commands?
Agent guidance for printing-press workflows.
Who is it for?
Developers automating printing press via agent-guided SKILL.md workflows.
Skip if: Skip when unrelated tooling already covers the task without this skill's documented flow.
When should I use this skill?
User asks about printing-press
What you get
Repeatable printing-press workflows with grounded commands and expected outputs.
- Ship-ready CLI
- Build artifacts
- Shipcheck validation report
By the numbers
- Skill version 2.0.0 with min-binary-version 4.0.0
- Allows 9 agent tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, Agent
Files
/printing-press
Generate the best useful CLI for an API without burning an hour on phase theater.
/printing-press Notion
/printing-press Discord codex
/printing-press --spec ./openapi.yaml
/printing-press --har ./capture.har --name MyAPI
/printing-press https://postman.com/explore
/printing-press https://postman.comWhat Changed In v2
The old skill inflated the path to ship:
- too many mandatory research documents before code existed
- too many separate late-stage validation phases after code existed
- too many chances to discover obvious failures late
This version uses one lean loop: 1. Resolve the spec and write one research brief 2. Generate 3. Build the highest-value gaps 4. Run one shipcheck block 5. Optionally run live API smoke tests
Artifacts are still written, but only the ones that materially help the next step.
Modes
Default
Normal mode. Claude does research, generation orchestration, implementation, and verification.
Codex Mode
If the arguments include codex or --codex, offload pure code-writing tasks to Codex CLI.
Use Codex for:
- writing store/data-layer code
- writing workflow commands
- fixing dead flags / dead code / path issues
- README cookbook edits
Keep on Claude:
- research and product positioning
- choosing which gaps matter
- verification results and ship decisions
If Codex fails 3 times in a row, stop delegating and finish locally.
Polish Mode (Standalone Skill)
For second-pass improvements to an existing CLI, use the standalone polish skill:
/printing-press-polish redfinSee the printing-press-polish skill for details. It runs diagnostics, fixes verify failures, removes dead code, cleans up descriptions and README, and offers to publish.
Rules
- Do not ship a CLI that hasn't been behaviorally tested against real targets.
go buildandverifypass-rate are structural signals, not correctness signals. Phase 5's mechanical test matrix runs every subcommand +--json+ error paths; if that matrix was not executed, the CLI is not shippable. Quick Check is the floor; Full Dogfood is required when the user asks for thoroughness. - Bugs found during dogfood are fix-before-ship, not "file for v0.2". If a 1-3 file edit resolves it, do it now.
ship-with-gapsis deprecated as a default verdict (see Phase 4). Context is freshest in-session; a v0.2 backlog that may never be revisited ships known-broken CLIs. - Features approved in Phase 1.5 are shipping scope. Do not downgrade a shipping-scope feature to a stub mid-build. If implementation becomes infeasible, return to Phase 1.5 with a revised manifest and get explicit re-approval.
- Do not quote human-time estimates for sub-tasks ("~15-30 min", "~1 hour", "quick fix") in
AskUserQuestionoptions, phase descriptions, or reference docs. The agent does the work, not the user; agent-fabricated estimates are notoriously bad and train users to distrust the prompt. Describe scope instead (lines of code, files touched, relative size). The carve-outs are wall-clock estimates for genuinely time-bound things: the whole-CLI run (set the user's expectation up front — most CLIs take 30+ minutes), tool installs (go installtakes ~10 seconds), and printing-press subcommands that do network-bound work (crowd-sniff scans npm + GitHub, ~5-10 minutes). Anything bounded by agent reasoning time is not time-bound — describe scope. - Optimize for time-to-ship, not time-to-document.
- Reuse prior research whenever it is already good enough.
- Do not split one idea across multiple mandatory artifacts.
- Durable files produced by this skill go under
$PRESS_RUNSTATE/(working state) or `$
Absorb Scoring Rubric: Novel Features
When to read: This file is the scoring rubric for the novel-features
subagent (Phase 1.5 Step 1.5c.5). It defines kill/keep checks, the
4-dimension score, the transcendence table format, and reprint verdict
rules. The subagent reads this file as part of its operational playbook —
the SKILL no longer applies it inline.
>
Brainstorming flow (personas, candidate generation, gap analysis, cut pass)
lives in novel-features-subagent.md. This
file owns the static rubric only.
Kill/keep checks
Apply these to every candidate BEFORE scoring. Cut ruthlessly — survivors must be features that can actually ship.
| Check | Kill condition | Keep/reframe action |
|---|---|---|
| LLM dependency | Feature requires NLP, summarization, sentiment analysis, classification, or semantic grouping | Reframe as mechanical: replace "summarize" with "extract top-rated items + stats." Add pipe-friendly output so users can `\ |
| External service | Feature requires a service not in the spec (e.g., scraping a website, calling a third-party API not in the brief) | Cut unless the service is free, public, and has no auth. An enrichment API documented in the brief (like OMDb for movie-goat) is fine. |
| Auth the user doesn't have | Feature requires write access, OAuth scopes, or paid tiers the user hasn't confirmed | Gate behind an auth check, or cut if the feature is useless without it. Read-only features using the same auth as other commands are fine. |
| Scope creep | Feature is really an application, not a command. Would take >200 lines to implement, needs a TUI, or requires persistent background processes. | Descope to the one-command version. "Dashboard" → "summary stats." "Monitor" → "poll once with --watch." If the one-command version isn't useful, cut. |
| Verifiability | Feature can't be tested in dogfood. No way to verify the output is correct without manual inspection or domain expertise. | Flag as low-confidence. Keep only if the value is high enough to justify manual QA. |
| Reimplementation | Feature synthesizes API responses locally instead of calling the API. Hand-rolled response builders, hardcoded JSON returned as an "API result," endpoint stubs that return constants, or aggregations computed in-process when the API has an aggregation endpoint. | Cut or rewrite. A printed CLI that pretends to call the API is strictly worse than the API call it replaces. Local SQLite commands (stale, bottleneck, health, reconcile) are local-data commands, not fake API calls. Curated static-reference commands must use // pp:novel-static-reference. Commands that call a real API through a wrapper dogfood cannot see must use // pp:client-call; never use that directive for hardcoded payloads or fake stubs. Dogfood's reimplementation_check enforces this at generation time. |
Buildability proof. For each surviving feature, write one sentence:
"This uses [specific API endpoint or local data] to compute [specific output] with no external dependencies."
If you can't write that sentence, the feature fails the vet.
Score (4 dimensions, raw 0-10)
Apply to candidates that survive the kill/keep checks.
| Dimension | Points | Scoring |
|---|---|---|
| Domain Fit | 0-3 | 3=core to this API's power users, 2=useful but niche, 1=tangential, 0=wrong domain |
| User Pain | 0-3 | 3=research surfaced explicit demand (community complaints, competitor gap), 2=implied need, 1=speculative, 0=no evidence |
| Build Feasibility | 0-2 | 2=API endpoint + local data covers it, 1=needs minor data model additions, 0=requires new infrastructure |
| Research Backing | 0-2 | 2=evidence from 2+ sources in Phase 1/1.5 research (web search, community issues, MCP source, DeepWiki analysis each count as 1 source), 1=evidence from 1 source, 0=invented |
Normalize: score_10 = round(raw / 10 * 10). Include features scoring >= 5/10.
Transcendence Table Format
Survivors render as rows in the absorb manifest's transcendence table:
| # | Feature | Command | Score | Buildability | How It Works | Evidence | Long Description |
|---|---------|---------|-------|--------------|--------------|----------|------------------|
| N | Player comparison | compare "LeBron" "Curry" | 8/10 | hand-code | Joins player_stats + team + season tables in local SQLite | ESPN community requests, espn_scraper lacks cross-player queries | Use this command to compare two players. Do NOT use it for team-level comparisons; use 'team compare' instead. |The "How It Works" column is the buildability proof — one sentence showing the specific API endpoint or local data that powers the feature.
The "Buildability" column tags whether the generator auto-emits the feature from the spec (spec-emits) or whether the agent must hand-write the Cobra file plus root.go wiring after generate (hand-code). See Pass 3 question 5 in novel-features-subagent.md for the classification rules. The Phase Gate 1.5 prose showcase counts hand-code rows from this column when reading out the hand-code commitment.
The "Evidence" column MUST cite specific findings from Phase 1 or Phase 1.5 research. "Power users would love this" is not evidence.
The "Long Description" column is none unless the feature overlaps with a sibling command closely enough that an agent may choose the wrong tool. When it is populated, it is implementation guidance for the hand-coded Cobra Long field and must reference only command names that survived the Pass 3 cut.
Reprint verdict rules
Reprints occur when a prior research.json exists at $PRESS_LIBRARY/<api>/research.json (provenance) or under $PRESS_MANUSCRIPTS/<api-slug>/*/research.json (archived runs). The subagent loads prior novel_features (planned) and novel_features_built (shipped), re-scores each against the current personas, and tags every prior feature with exactly one verdict:
| Verdict | When | Pool action |
|---|---|---|
| Keep | Persona fit, score ≥ 5/10, buildable | Add with prior command reused so the reprint stays compatible |
| Reframe | Right idea, wrong shape — persona fit exists but command/scope drifted | Add with a new command/description; flag the rename |
| Drop | No persona fit, score < 5/10, or unbuildable now | Exclude; record one-line reason for the manifest |
Surface in the manifest. Reprint runs add a Source column to the transcendence table: prior (kept), prior (reframed from <old-command>), or new. Below the table, list dropped prior features with their one-line justifications so the user can override the drop at the Phase 1.5 gate review. Prior features are never silently absorbed and never silently dropped.
Multi-source aggregator pattern
Use this path when the CLI's value comes from combining multiple upstream sources, but one source can still seed a normal Printing Press generation run. This is the third path between "browser-sniff a spec" and "hand-write the whole module": generate from the dominant source, then add secondary source clients by hand.
Good fits:
- One source has an OpenAPI/spec/HAR capture that can produce the baseline CLI.
- Additional sources are small, wrapper-only, HTML/RSS/JSON APIs, or otherwise
better implemented as focused clients.
- The user-facing model is unified across sources, such as a single
works,
places, events, or roasters store.
- The extra sources add coverage, ranking, enrichment, or cross-source compare
commands rather than replacing the generated baseline.
Bad fits:
- No source can seed generation. Hand-write the module instead.
- Each source needs a completely different resource model. Build separate CLIs
or reduce scope.
- The secondary sources are only examples in docs and are not reachable enough
to smoke test.
Reference layout
Keep the hand-authored source layer outside generator-owned packages:
internal/source/source.go
internal/source/registry.go
internal/source/<slug>/client.go
internal/source/<slug>/sync.go
internal/cli/sources.gointernal/source/source.go defines the shared contract:
type Work struct {
Source string
ID string
Title string
URL string
}
type SyncOptions struct {
Limit int
}
type Source interface {
Name() string
Description() string
AuthRequired() bool
Sync(context.Context, SyncOptions) ([]Work, error)
}internal/source/registry.go owns registration and lookup:
var (
registryMu sync.RWMutex
registry = map[string]Source{}
)
func Register(source Source) {
registryMu.Lock()
defer registryMu.Unlock()
registry[source.Name()] = source
}
func All() []Source {
registryMu.RLock()
out := make([]Source, 0, len(registry))
for _, source := range registry {
out = append(out, source)
}
registryMu.RUnlock()
sort.Slice(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
return out
}
func Lookup(name string) (Source, bool) {
registryMu.RLock()
defer registryMu.RUnlock()
source, ok := registry[name]
return source, ok
}Each internal/source/<slug>/ package registers itself from init() and keeps HTTP/API details private to that source. The mutex keeps copied test helpers and late registrations race-safe; production registration should still happen from init().
Runtime rules
- Every source client must make real external calls. Mark intentional outbound
calls with the existing // pp:client-call annotation when dogfood cannot infer them.
- Read per-source-rate-limiting.md before
writing clients. Each source needs its own limiter and typed 429 behavior.
- Do not edit generated
internal/clientcode for secondary sources. Add a
sibling package instead so regeneration keeps the generated client clean.
- Keep generated endpoint commands intact; add aggregator commands beside them.
Unified store
Create one lossy cross-source table for the domain entity, such as works. Include source identity in the key so IDs do not collide:
CREATE TABLE works (
source TEXT NOT NULL,
source_id TEXT NOT NULL,
title TEXT NOT NULL,
url TEXT,
raw_json TEXT,
PRIMARY KEY (source, source_id)
);If adding FTS5 manually, either use a contentless table with explicit inserts or copy the generator's contentful table plus INSERT/UPDATE/DELETE trigger shape from store.go.tmpl. Do not create a contentful FTS5 table without the matching triggers.
Command surface
Add a sources command tree:
sources listprints registered source names, descriptions, and auth needs.sources sync [--source <name>] [--limit N]runs one source or all sources.- Domain commands (
browse,similar,compare,search) query the unified
store, not individual source packages directly.
Annotate read-only commands, especially sources list, with cmd.Annotations["mcp:read-only"] = "true" so MCP hosts do not request write permission for registry inspection.
The command tree should make source boundaries visible without requiring the user to know implementation package names.
Conformance check
Before shipping an aggregator CLI, verify:
internal/source/source.godefines the shared entity andSourcecontract.internal/source/registry.goexposes register/list/lookup behavior.- Each source lives under
internal/source/<slug>/. - Each source client has a real outbound call and per-source rate limiting.
sources listandsources syncexist.sources listis annotatedmcp:read-only.- Cross-source commands read from the unified store.
- The README and SKILL explain which sources are included and which command
syncs them.
Known instances that motivated this pattern:
coffee-goat: generated baseline plus hand-authored coffee review, Shopify,
and YouTube source clients.
art-goat: generated baseline plus hand-authored Art Institute of Chicago
and APOD source clients.
press-auth Companion: Cookie Capture for Cookie/Composed-Auth CLIs
When to read: Pre-Generation Auth Enrichment (Phase 1.7 / Phase 2) for any spec
withauth.typeofcookieorcomposed. Also load when the user reports that a
printed CLI's auth login --chrome exits with "Required cookies not in Chrome'scookie DB" or any cookie-extraction failure. The Printing Press's
Cookie/composed HTML transport section in SKILL.md points here for the recommendation
flow and debug playbook.
What press-auth is
press-auth is a companion binary that ships from cmd/press-auth/ in cli-printing-press. It captures cookies once via a controlled Chrome window (separate user-data-dir, never the user's daily profile), stores per-domain encrypted state, and serves cookie headers to generated CLIs on demand. Lazy JWT refresh keeps sessions alive past short Auth0/refresh-cookie expiries without a background daemon. macOS only for v1; Linux and Windows are tracked as follow-up work.
It exists because the legacy auth login --chrome chain (pycookiecheat -> cookies -> browser-use --connect -> raw CDP) cannot reach the session cookies that hold modern session/JWT tokens. Chrome keeps session cookies in RAM, the on-disk Cookies DB never sees them, and the user's daily Chrome isn't launched with --remote-debugging-port=N. press-auth sidesteps both constraints by spawning its own controlled Chrome.
When the press skill should recommend installing press-auth
Recommend press-auth in user-facing output when any of these are true for the current run:
- The spec's
auth.typeiscookieorcomposed(typical for sniffed APIs:
loyalty programs, airline portals, vacation sites, retail accounts).
- The user is hitting "Required cookies not in Chrome's cookie DB (likely
session cookies). Attempting to read from live Chrome session..." from an existing CLI's auth login --chrome.
- The spec carries the new
login_urlandjwt_carrier_cookiehints (the
spec-extension keys introduced alongside this companion) — those make press-auth's setup a single command.
- The user wants a CLI that "just works" across sessions without re-authing.
Do NOT recommend it when:
auth.typeisapi_key,bearer_token, oroauth2_client_credentials
with a token URL — those paths are already first-class and don't need cookie capture.
- The user is on Linux or Windows. v1 is macOS-only. Surface that limitation
upfront; don't have them install and then discover the keychain error.
First-time install for the user
Two-step install, copy-pasted into the user's terminal:
go install github.com/mvanhorn/cli-printing-press/v4/cmd/press-auth@latest
press-auth login <domain> \
--login-url <login-url> \
--jwt-carrier-cookie <carrier> \
[--complete-selector <selector>] \
[--refresh-endpoint <path>]<domain> is the cookie domain (e.g., alaskaair.com), not a URL. <login-url> is the page where the user signs in. <carrier> is the cookie that holds the JWT for expiry tracking.
The first press-auth login for a given domain triggers a macOS keychain prompt: "press-auth wants to use your confidential information stored in the keychain." Tell the user to click Always Allow (not just "Allow") — the prompt repeats on every read otherwise. press-auth itself also prints a one-line heads-up before the keychain write.
How press-auth fits into the generated CLI's auth flow
The press's auth_browser.go.tmpl (touched by U6 of the bug-fix plan) emits an auth.Header() flow that tries press-auth first:
1. Step 0 (preferred): exec.LookPath("press-auth"). If found, shell out to press-auth cookies <domain>. On success, return the cookie header in under 100ms. On failure with state present, surface the press-auth error verbatim (it names the next command). 2. Fallback (legacy): the existing detectCookieTool -> extractCookies -> extractLiveCookies chain. Unchanged from pre-fix behavior. 3. Final error (when all paths fail): message names press-auth as the recommended install — go install ... && press-auth login <domain>.
The fallback exists so existing CLIs and users without press-auth installed keep working. There is no flag-day migration; the press doesn't require press-auth at runtime.
Debugging capture failures
| Symptom | Cause | Fix |
|---|---|---|
| "No Chrome found" | chromedp can't locate the Chrome binary. | Install Google Chrome at the default location (/Applications/Google Chrome.app). chromedp auto-detects from there. |
| Login window opens, user signs in, but capture never completes (10-min timeout) | Heuristic completion check (URL-changed + no password input + signout link present) didn't match the post-login DOM. | Re-run press-auth login <domain> with an explicit --complete-selector pointing at an element that only appears after login: a[href*=signout], [data-testid=user-menu], nav .account-name. |
press-auth cookies <domain> returns 401 after weeks of working | Refresh window exceeded; the underlying session is dead. | Re-run press-auth login <domain>. Lazy refresh only extends sessions while the carrier cookie is still refreshable. |
Keychain prompt repeats on every press-auth cookies call | User clicked "Allow" instead of "Always Allow" on the first prompt. | press-auth forget <domain> then press-auth login <domain> again. Choose Always Allow. |
press-auth cookies exits 5 with "refresh returned no new cookies" | Refresh endpoint responded 200 but didn't rotate the carrier cookie (some APIs only rotate on specific paths). | Verify the spec's refresh_endpoint is the correct path. If the API has a separate "extend session" vs "exchange token" endpoint, the spec needs the token-exchange path. |
press-auth cookies exits 3 with "could not decode JWT exp claim" | The --jwt-carrier-cookie value doesn't actually carry a JWT (e.g., it's an opaque session ID). | Inspect the cookie value during browser-sniff. If it's header.payload.signature-shaped (three base64 segments), it's a JWT. Otherwise pick a different carrier cookie or drop refresh and re-login when sessions die. |
State file works locally but auth login --chrome in a generated CLI still falls through to the legacy chain | The generated CLI was emitted BEFORE the U6 template change landed, or the user has an old binary on PATH. | Regenerate the printed CLI from cli-printing-press main; which <api-slug>-pp-cli to confirm the on-PATH binary is the regenerated one. |
Scoping login URLs and selectors per API
The three new spec fields (login_url, login_complete_selector, jwt_carrier_cookie) drive press-auth's one-command setup. Get them right during Phase 1.7 (browser-sniff) and the generated CLI's auth login --chrome becomes a single command for the end user.
login_url
The page where the user enters credentials. Usually one of:
https://<site>/account/loginhttps://<site>/signinhttps://login.<site>/- An IdP redirect (
https://<site>/auth/<provider>/start)
Discovery: open the upstream brand's main site nav and click "Sign in". The final URL after redirects is what press-auth should navigate to.
Validation: must be HTTPS. press-auth refuses HTTP login URLs (a cookie captured over plain HTTP isn't worth storing in a keychain-protected file).
login_complete_selector (optional but strongly recommended)
A CSS selector that's only visible AFTER login. Without it, press-auth uses a generic heuristic (URL changed + no input[type=password] + signout link present), which fires reliably on most sites but can hang on SPAs with client-side post-login redirects.
Good signals:
- A signout link:
a[href*=signout],a[href*=logout] - An account menu trigger:
[data-testid=user-menu],
[aria-label="Account menu"]
- A welcome banner:
.welcome-name,[data-test=account-greeting] - A logged-in-only nav item:
nav a[href="/dashboard"]
Avoid selectors that match the marketing page or pre-login state — they'll match too early and capture half-baked cookies.
jwt_carrier_cookie
The cookie whose value holds the JWT used for expiry tracking. press-auth decodes the exp claim from this cookie's body to know when to refresh.
Discovery during Phase 1.7:
1. After login, inspect the browser's request to an authenticated endpoint. 2. Read the Cookie header values. The carrier is usually a cookie with a value shaped like eyJhbGciOi...XXXXX.YYYYY (three dot-separated base64 segments) OR a URL-encoded JSON wrapper containing that. 3. Common names: session, auth, guestsession, token, id_token, access_token. Some sites use brand-specific names like AS_ACNT/guestsession (Alaska), iC_session (Iceland Air), etc.
If multiple cookies carry JWTs (e.g., id + access), pick the access token carrier — that's the one the API actually validates and rotates on refresh.
If the spec only declares cookie_domain and cookies (no jwt_carrier_cookie), press-auth still captures and serves cookies but skips lazy refresh. Sessions die when the API stops accepting the original cookie set; the user has to re-login. Acceptable for short-lived workflows; not great for daily-driver CLIs.
Honest limits
- macOS only in v1. Linux uses
libsecret/gnome-keyring, Windows uses
DPAPI; both are tracked as follow-up plans. Don't promise cross-platform yet.
- No background daemon. Refresh happens lazily on
press-auth cookies
read. If the user runs a CLI command after a long idle, the first call may block on a refresh round-trip (typically <500ms).
- One account per domain. press-auth v1 doesn't support multi-profile
switching (e.g., personal vs work accounts on the same domain). press-auth forget <domain> and re-login swaps. Multi-profile is on the v2 wish list.
- chromedp + Chrome version coupling. If Google ships a Chrome update
that breaks chromedp's protocol assumptions, press-auth login will fail until chromedp is bumped. Run press-auth login with --verbose to see the chromedp protocol error; report upstream.
Cross-references
- `references/browser-sniff-capture.md` — where
login_url, login_complete_selector, and jwt_carrier_cookie are discovered during Phase 1.7 traffic capture.
- `cmd/press-auth/` — the binary's source.
- `internal/pressauth/` — the supporting
package (state file, keychain, chromedp launcher, JWT decode, refresh).
- `internal/generator/templates/auth_browser.go.tmpl` —
the template that integrates press-auth into generated CLIs.
- `docs/solutions/logic-errors/auth-login-chrome-broken-2026-05.md` —
bug history: the original failure, the root cause, the press-auth fix.
- `docs/SPEC-EXTENSIONS.md` — canonical
reference for the spec keys (login_url, login_complete_selector, jwt_carrier_cookie, and the OpenAPI x-auth-companion form).
Codex Delegation
When to read: This file is referenced by Phase 3 and Phase 4 of the printing-press skill.
Read it when CODEX_MODE is true to delegate code-writing and bug-fix tasks to Codex CLI.IMPORTANT: Delegate via echo $PROMPT | codex exec in Bash. Do NOT use the Skill tool with codex:codex-cli-runtime - that skill is only for the rescue subagent, not general delegation.
Phase 3: Codex Delegation
When CODEX_MODE is true, delegate code-writing tasks to Codex CLI. Claude still decides WHAT to build and in what order. Codex does the hands — writing Go functions.
Delegation loop for each priority task:
1. Decompose the current priority level into discrete tasks (one per command/feature from the absorb manifest).
2. For each task, follow this delegation cycle:
a. Read context — Read the relevant source files from the generated CLI to extract actual code for the prompt. Use head -50, grep -A 20, or cat to get real code, not descriptions.
b. Snapshot — Create a clean restore point before Codex writes anything:
cd "$PRESS_LIBRARY/<api>-pp-cli" && git add -A && git stash push -m "pre-codex-task"c. Assemble prompt — Build a CODEX_PROMPT using the appropriate task type template (see below).
d. Delegate — Remove any stale completion marker from a prior task, then pipe to Codex:
# Model and reasoning effort inherit from ~/.codex/config.toml. Do not pin -m / -c here.
cd "$PRESS_LIBRARY/<api>-pp-cli" && rm -f _codex-result.json && echo "$CODEX_PROMPT" | codex exec \
--yolo \
-e. Validate — Check the completion marker first (self-describing on failure), then the build:
cd "$PRESS_LIBRARY/<api>-pp-cli"
if [ ! -f _codex-result.json ] || [ "$(jq -r '.status // empty' _codex-result.json 2>/dev/null)" != "complete" ]; then
echo "codex output marker missing — partial work may be present in $PWD; review before continuing" >&2
false
else
go build ./... && go vet ./...
fiAlso verify git diff --stat shows a non-empty diff.
A missing or non-complete marker means Codex exited before finishing the prompt (sandbox abort, OOM, SIGINT, internal toolchain crash); the if branch prints the diagnostic itself so the agent does not have to second-guess which arm failed. Fall through to step (g) as a failure. Do NOT proceed to the next priority task with whatever Codex managed to write before bailing.
f. On success — Discard the restore point and reset the failure counter:
git stash drop 2>/dev/nullSet CODEX_CONSECUTIVE_FAILURES=0.
g. On failure (build fails, vet fails, empty diff, or Codex error) — Revert and fall back:
git checkout -- . && git stash pop 2>/dev/nullIncrement CODEX_CONSECUTIVE_FAILURES. Claude implements this task directly (standard non-codex path).
h. Circuit breaker — If CODEX_CONSECUTIVE_FAILURES reaches 3:
echo "Codex disabled after 3 consecutive failures — completing in standard mode."
CODEX_MODE=falseAll remaining tasks in Phase 3 (and Phase 4) use Claude directly.
3. After each priority level, run the same quality checks as non-codex mode (e.g., Priority 1 Review Gate).
Task type prompt templates:
All templates follow this structure. Paste ACTUAL CODE in the CURRENT CODE section — never descriptions of code.
All CONVENTIONS lists apply the empty-collection rule to every list-shaped output: initialize empty result slices so JSON output is [], not null.
Store table task:
TASK: Add <entity> table with Upsert and Search methods to the SQLite store.
FILES TO MODIFY:
- internal/store/store.go
CURRENT CODE (existing table pattern):
$(grep -A 30 "CREATE TABLE IF NOT EXISTS" internal/store/store.go | head -40)
EXPECTED CHANGE:
Create a new table for <entity> with columns: <fields from spec>.
Add Upsert<Entity>(ctx, item) and Search<Entity>(ctx, query) methods following the existing pattern.
Add FTS5 virtual table if entity has searchable text fields.
CONVENTIONS:
- Package: store
- Use the same CreateTable/Upsert/Search pattern as existing tables
- Error handling: return fmt.Errorf("upsert <entity>: %w", err)
- All table names are snake_case
CONSTRAINTS:
- Do NOT run git commit, git push, or git add
- Do NOT modify files outside internal/store/store.go
- Keep changes under 200 lines
- Run: go build ./... && go vet ./...
VERIFY: After making changes, run:
cd . && go build ./... && go vet ./...
COMPLETION MARKER: After VERIFY succeeds (build green, vet clean), and only then, write a JSON object to _codex-result.json in the cwd:
{"status":"complete","files_written":["internal/store/store.go"],"timestamp":"<ISO 8601 UTC, e.g. 2026-05-16T12:00:00Z>"}
If you bail, hit a fatal error, or VERIFY fails, do NOT write this file. The parent skill uses its presence and "status":"complete" as the only signal that the prompt ran end-to-end; missing marker is treated as a partial-completion failure regardless of process exit code.Workflow command task:
TASK: Create the <command> subcommand for <api>-pp-cli.
FILES TO MODIFY:
- internal/cli/<command>.go (create new)
CURRENT CODE (cobra command pattern from an existing command):
$(cat internal/cli/<existing-command>.go | head -60)
CURRENT CODE (root command registration):
$(grep -A 5 "AddCommand" internal/cli/root.go)
EXPECTED CHANGE:
Create a <command> command that:
<plain English description of what the command does, from the absorb manifest>
Must support: --json, --select, --compact, --limit, --dry-run (for mutations).
Must have realistic --help examples with domain-specific values.
CONVENTIONS:
- EMPTY-COLLECTION CONVENTION:
When the command's primary output is a list/array/cluster, declare it
as `results := make([]T, 0)` (not `var results []T`). Nil-slice marshals
to JSON `null`, which breaks `jq '.[]'` agent pipelines; an initialized
empty slice marshals to `[]` and lets downstream consumers iterate
uniformly across empty and non-empty results.
- Package: cli
- Use cobra.Command pattern matching existing commands
- Error handling: return fmt.Errorf with context
- Progress output: fmt.Fprintf(os.Stderr, ...)
- Register with rootCmd.AddCommand in root.go
CONSTRAINTS:
- Do NOT run git commit, git push, or git add
- Do NOT modify files outside internal/cli/<command>.go and internal/cli/root.go
- Keep changes under 200 lines per file
- Run: go build ./... && go vet ./...
VERIFY: After making changes, run:
cd . && go build ./... && go vet ./...
COMPLETION MARKER: After VERIFY succeeds (build green, vet clean), and only then, write a JSON object to _codex-result.json in the cwd:
{"status":"complete","files_written":["internal/cli/<command>.go","internal/cli/root.go"],"timestamp":"<ISO 8601 UTC>"}
If you bail, hit a fatal error, or VERIFY fails, do NOT write this file. The parent skill uses its presence and "status":"complete" as the only signal that the prompt ran end-to-end; missing marker is treated as a partial-completion failure regardless of process exit code.Transcendence command task:
TASK: Create the <command> transcendence command — a compound query across local SQLite data.
FILES TO MODIFY:
- internal/cli/<command>.go (create new)
CURRENT CODE (available store methods):
$(grep -E "^func \(db \*DB\)" internal/store/store.go | head -20)
CURRENT CODE (cobra pattern):
$(cat internal/cli/<existing-command>.go | head -40)
EXPECTED CHANGE:
Create a <command> command that:
<plain English description — what entities it joins, what insight it produces>
This command ONLY works because all data is in local SQLite.
Must support: --json, --select, --compact, --limit.
CONVENTIONS:
- EMPTY-COLLECTION CONVENTION:
When the command's primary output is a list/array/cluster, declare it
as `results := make([]T, 0)` (not `var results []T`). Nil-slice marshals
to JSON `null`, which breaks `jq '.[]'` agent pipelines; an initialized
empty slice marshals to `[]` and lets downstream consumers iterate
uniformly across empty and non-empty results.
- Package: cli
- Query across tables using db methods, not raw SQL in CLI layer
- Format output as a table by default, JSON with --json
CONSTRAINTS:
- Do NOT run git commit, git push, or git add
- Do NOT modify files outside internal/cli/<command>.go and internal/cli/root.go
- Keep changes under 200 lines per file
- Run: go build ./... && go vet ./...
VERIFY: After making changes, run:
cd . && go build ./... && go vet ./...
COMPLETION MARKER: After VERIFY succeeds (build green, vet clean), and only then, write a JSON object to _codex-result.json in the cwd:
{"status":"complete","files_written":["internal/cli/<command>.go","internal/cli/root.go"],"timestamp":"<ISO 8601 UTC>"}
If you bail, hit a fatal error, or VERIFY fails, do NOT write this file. The parent skill uses its presence and "status":"complete" as the only signal that the prompt ran end-to-end; missing marker is treated as a partial-completion failure regardless of process exit code.Phase 4: Codex Delegation (Fixes)
When CODEX_MODE is true, delegate each bug fix to Codex. The shipcheck tools themselves (dogfood, verify, scorecard) always run on Claude — they are Go binary executions. Only the CODE FIXES are delegated.
For each bug identified from dogfood/verify/scorecard output:
1. Read the finding — identify the exact file, the issue, and what needs to change.
2. Read the code — extract the actual broken code for context:
grep -n -A 10 "<broken pattern or function name>" "$PRESS_LIBRARY/<api>-pp-cli/<file>"3. Snapshot before Codex writes:
cd "$PRESS_LIBRARY/<api>-pp-cli" && git add -A && git stash push -m "pre-codex-fix"4. Assemble and delegate using the fix prompt template:
TASK: Fix <finding summary from dogfood/verify>.
FILES TO MODIFY:
- <exact file path>
CURRENT CODE (the broken section):
<actual code from the file — use grep -A or head/tail, not descriptions>
BUG:
<the dogfood/verify finding, verbatim>
EXPECTED FIX:
<plain English description of the correct behavior>
CONSTRAINTS:
- Do NOT run git commit, git push, or git add
- Do NOT modify files outside the listed path
- Keep changes under 50 lines
- Run: go build ./... && go vet ./...
VERIFY: After making changes, run:
cd . && go build ./... && go vet ./...
COMPLETION MARKER: After VERIFY succeeds (build green, vet clean), and only then, write a JSON object to _codex-result.json in the cwd:
{"status":"complete","files_written":["<exact file path>"],"timestamp":"<ISO 8601 UTC>"}
If you bail, hit a fatal error, or VERIFY fails, do NOT write this file. The parent skill uses its presence and "status":"complete" as the only signal that the prompt ran end-to-end; missing marker is treated as a partial-completion failure regardless of process exit code. # Model and reasoning effort inherit from ~/.codex/config.toml. Do not pin -m / -c here.
cd "$PRESS_LIBRARY/<api>-pp-cli" && rm -f _codex-result.json && echo "$CODEX_PROMPT" | codex exec \
--yolo \
-5. Validate — same as Phase 3: check the completion marker first (self-describing on failure), then build and vet:
cd "$PRESS_LIBRARY/<api>-pp-cli"
if [ ! -f _codex-result.json ] || [ "$(jq -r '.status // empty' _codex-result.json 2>/dev/null)" != "complete" ]; then
echo "codex output marker missing — partial work may be present in $PWD; review before continuing" >&2
false
else
go build ./... && go vet ./...
fiAlso verify git diff --stat shows a non-empty diff. The if branch prints the diagnostic itself before exiting non-zero, so the agent does not have to second-guess which arm failed; fall through to step 7.
6. On success — git stash drop, reset CODEX_CONSECUTIVE_FAILURES=0.
7. On failure — git checkout -- . && git stash pop, increment CODEX_CONSECUTIVE_FAILURES, Claude fixes this bug directly.
8. Circuit breaker — shares the same counter from Phase 3. If already disabled, all fixes use Claude.
Crowd-Sniff Implementation
When to read: This file is referenced by Phase 1.8 of the printing-press skill.
Read it when the user approves crowd-sniff (mining npm SDKs and GitHub code search).
If user approves crowd-sniff
Ensure the discovery directory exists:
mkdir -p "$DISCOVERY_DIR"Run the crowd-sniff command and capture both the spec and JSON provenance:
cli-printing-press crowd-sniff --api <api> --output "$RESEARCH_DIR/<api>-crowd-spec.yaml" --json > "$DISCOVERY_DIR/crowd-sniff-provenance.json"If the API has a known base URL from Phase 1 research, pass it:
cli-printing-press crowd-sniff --api <api> --base-url <known-base-url> --output "$RESEARCH_DIR/<api>-crowd-spec.yaml" --json > "$DISCOVERY_DIR/crowd-sniff-provenance.json"Report the results: "Crowd-sniff discovered N endpoints across M resources (X from npm, Y from GitHub)."
Feed into Phase 2:
- Enrichment mode: Phase 2 will use
--spec <original> --spec <crowd-spec> --name <api>to merge both - Primary mode: Phase 2 will use
--spec <crowd-spec>directly
Write crowd-sniff discovery report
Write a structured crowd-sniff provenance report to $DISCOVERY_DIR/crowd-browser-sniff-report.md. This report preserves the discovery evidence so a future maintainer can see what community sources informed the spec.
The report must contain these sections:
1. npm Packages Analyzed — List each SDK package examined: name, version, download count, recency. Note which packages yielded endpoints and which were empty/irrelevant.
2. GitHub Repos Searched — The search queries used, repos matched, and freshness of each repo. Note the GitHub token status (authenticated with broader results, or unauthenticated with rate-limited results).
3. Endpoints Discovered — A markdown table with columns: Method, Path, Source Tier (official-sdk / community-sdk / code-search), Source Count (seen in N independent sources). Sorted by source tier then frequency.
4. Base URL Resolution — Candidates discovered and which was selected, with rationale (e.g., "Found in 3 npm packages: https://api.notion.com").
5. Auth Patterns Detected — Authentication patterns found in SDK code (API key headers, bearer tokens, OAuth flows). Include the header name or env variable convention when visible.
6. Parameter Name Evidence — SDK argument names, wrapper function names, examples, form labels, or source comments that clarify cryptic wire keys. Preserve endpoint-local evidence so spec enrichment can author flag_name without guessing.
7. Coverage Summary — Total endpoints found, breakdown by source tier, and any gaps compared to the Phase 1 research brief (e.g., "Brief mentions webhooks but no webhook endpoints found in community code").
DeepWiki Codebase Analysis
When to read: This file is referenced by Phase 1.5a.6 of the printing-press skill.
Read it when GitHub repos have been discovered during Phase 1/1.5a research.
What DeepWiki Provides
DeepWiki generates AI-analyzed documentation for any public GitHub repo. It explains how a codebase works - architecture, data models, auth flows, error handling - not just what endpoints exist. This complements crowd-sniff (which finds endpoints) and MCP source reading (which finds auth headers) with semantic understanding.
URL pattern: https://deepwiki.com/{owner}/{repo}
When to Query
Query DeepWiki when Phase 1 or Phase 1.5a discovers a GitHub repo URL for:
- The API's official SDK repo (e.g.,
stripe/stripe-node,triggerdotdev/trigger.dev) - The API's server repo (if open source)
- A popular community wrapper or MCP server repo
Repo URL sources: npm package metadata (repository field), GitHub search results, MCP server discovery (Step 1.5a searches), WebSearch results linking to GitHub repos.
Skip when:
- No GitHub repos were discovered
- The repo is private (DeepWiki only indexes public repos)
- The API is trivially simple (1-2 endpoints, no auth)
How to Query
Time budget: 2 minutes max. If DeepWiki is slow or returns errors, skip silently.
Step 1: Fetch the wiki structure
WebFetch: https://deepwiki.com/{owner}/{repo}
Prompt: "List all wiki section titles and their numeric path identifiers"This returns the wiki's table of contents with section IDs like 1-overview, 3.1-task-definition-api, 5.8-authentication-and-authorization.
Step 2: Fetch targeted sections
From the wiki structure, identify and fetch up to 3 of these high-value sections (in priority order):
1. Authentication/Authorization - look for sections with "auth", "authentication", "authorization", "security", "api key", "token" in the title. This reveals token formats, auth flows, required scopes, and session handling.
2. Data Model/Schema - look for sections with "model", "schema", "database", "data", "entity" in the title. This reveals entity relationships, primary keys, and data flow patterns.
3. Architecture/Overview - look for sections with "architecture", "overview", "system", "design" in the title. This reveals how components interact, what the main abstractions are, and how the API is structured internally.
4. API/SDK surface - look for sections with "api", "sdk", "client", "endpoint", "rest" in the title. This reveals the intended API surface from the maintainer's perspective.
For each section, use WebFetch with a targeted extraction prompt:
WebFetch: https://deepwiki.com/{owner}/{repo}/{section-path}
Prompt: "Extract: (1) authentication method and token format, (2) primary data
entities and their relationships, (3) rate limiting or throttling behavior,
(4) error handling patterns, (5) key architectural decisions. Be specific -
include field names, header names, status codes, and entity names."Step 3: Synthesize findings
Organize extracted intelligence into these categories:
- Auth flow: Token type (Bearer, API key, session cookie, OAuth), header name,
env var convention, required scopes or permissions
- Data model: Primary entities, their relationships (1:many, many:many),
key fields, pagination approach
- Rate limiting: Limits per endpoint or global, retry-after behavior,
backoff strategy
- Error patterns: Error response format, common error codes, retry guidance
- Architecture insights: Key abstractions, service boundaries, queue/worker
patterns, event systems
How to Use Findings
In the Research Brief (Phase 1)
Add findings to the ## Codebase Intelligence section (between Data Layer and User Vision). Only populate when DeepWiki returned useful findings:
## Codebase Intelligence
- Source: DeepWiki analysis of {owner}/{repo}
- Auth: [token type, header, env var pattern]
- Data model: [primary entities and relationships]
- Rate limiting: [limits and behavior]
- Architecture: [key insight about how the API works internally]In the Absorb Manifest (Phase 1.5b)
When attributing features in the absorb manifest's "Best Source" column, use DeepWiki ({owner}/{repo}) to distinguish DeepWiki-sourced intelligence from other sources.
In Novel Feature Scoring (Phase 1.5c.5)
DeepWiki architectural analysis counts as 1 evidence source for the "Research Backing" scoring dimension (0-2 points). Combined with another source (e.g., community issues, competitor tool analysis), it can push a feature to the maximum 2/2 Research Backing score.
Pre-Browser-Sniff Auth Intelligence (Phase 1.6)
DeepWiki auth findings directly feed into Phase 1.6's auth profile classification. If DeepWiki reveals the token format (e.g., tr_dev_ prefix for Trigger.dev, sk_live_ for Stripe), credential setup steps, or required scopes, use these to ask the user more specific auth questions.
Failure Handling
- DeepWiki returns 404 or error: Skip silently. Log: "DeepWiki: no wiki
available for {owner}/{repo}"
- WebFetch times out: Skip silently. Log: "DeepWiki: timeout fetching
{owner}/{repo}"
- Wiki exists but targeted sections not found: Extract what is available.
Even the overview section provides useful architectural context.
- Wiki content is thin or unhelpful: Use only findings that are specific
and actionable. Do not pad the brief with generic DeepWiki output.
Device Sniff BLE
Use this reference when the requested CLI target is a local physical device controlled over Bluetooth/BLE rather than a public HTTP API or website.
Routing
- Use
device-sniff bleas the durable discovery path. - Use
bluetooth-sniffas the discoverable alias when the user asks for Bluetooth directly. - Do not flatten BLE evidence into fake HTTP paths or methods.
- Do not make reusable artifacts vendor-specific. Product names belong only in examples, fixtures, or evidence labels.
Device Sniff is evidence-first. Community libraries, official docs, Android logs, Wireshark/nRF captures, and human action journals can guide discovery, but generated commands should remain tied to observed, replay-validated, or reference-backed BLE evidence.
Protocol Contract Synthesis Gate
BLE device discovery has a hard gate. A scan or service inspection tells you a device exposes characteristics, but not what its command payloads mean, nor how to operate the device. Before generating callable control commands or running a live write, synthesize the device's protocol contract from concrete sources — in one deliberate research pass, not command-by-command on hardware. The gate has three parts; satisfy all three.
1. The action map — what the bytes mean. Establish action -> service/characteristic/payload from a concrete source.
Accepted sources:
- A user-provided device spec, payload table, or action journal.
- Official docs, protocol notes, SDKs, or vendor examples.
- Community libraries, reverse-engineering notes, GitHub issues, forum posts, or examples that map command names to services, characteristics, and payload bytes.
- Android/iOS Bluetooth logs, btsnoop captures, Wireshark/nRF captures, or other external captures.
- A human action journal correlated with observed writes, where one action maps cleanly to one payload/characteristic candidate.
2. The operational contract — how to talk to the device. The same sources that give you payloads almost always document the operating contract too, and it is exactly the part that gets silently rediscovered on hardware if you skip it. Extract it into the spec's transport: block and quirks: list (see "Protocol Contract" below): write semantics, command spacing, connect ceremony and ordering, settle delays, poll cadence, teardown behavior, single-client constraints, and qualitative quirks (init tricks, stale-session gotchas, firmware-variant opcode shifts, notify-enable dances).
*3. The workflow spine — how the device is operated end to end. Commands and the operating contract are the parts; the workflows are how a working client assembles them into a sequence that actually achieves each user goal (start a walk, stop the belt, pair-then-arm). The reference encodes these sequences directly — the order of mode → settle → start, the wait-for-running before setting speed, the stop ceremony. Understanding individual commands is not enough; the spine* is where a from-scratch implementation goes wrong. Extract each primary goal's proven sequence into the spec's workflows: list ({name, goal, steps, evidence_refs}) — ordered and cited — so the QA fidelity pass and the dogfood checklist have a contract to check the implementation against. This is the part that, when left unwritten, gets rediscovered by guessing on hardware. Capture it once.
Required behavior:
- Run an explicit external web-research pass; do not stop at the seeds. A user-provided repo or doc is seed #1, not the finish line — go wider before you build. Web-search and GitHub-code-search by product name/model, advertised BLE name, service UUIDs, app package name, and known library names; pull Home Assistant / ESPHome integrations, vendor docs and protocol notes, issues, and forum threads. Find the most complete reference, and cross-check the contract across at least two independent sources when they exist — a wrapper that merely imports another library is not an independent source.
- Record what you consulted (a research ledger). In the discovery output, list the sources you actually examined — URL, what each contributed, how independent it is — so breadth is an artifact, not an assumption.
evidence_refsthat all trace back to the single repo the user handed you is a red flag: state plainly whether ≥2 independent sources corroborate the contract, or why only one credibly exists. - Cite reference repos; do not vendor them into the manuscript. Cloning a reference library to study its protocol is expected, but it is research input, not authored manuscript output. Download it to scratch outside the manuscript tree (e.g. a temp dir) and record only the URL + commit (in
evidence_refsand the ledger) — never copy a third-party repo intomanuscripts/<slug>/research/sources/. Publishing copies of someone else's code is a licensing problem and a secret/PII vector. Publish excludes anysources/from shipped manuscripts as a machine backstop, but keep it out in the first place. - Synthesize once, then build. Capture the action map into
capabilities, the operational contract intotransport:+quirks:, and the proven sequences intoworkflows:, each withevidence_refs, before writing the codec or touching hardware. - Don't relearn cited facts. If a source states a timing, ordering, or write-semantics fact (a command-spacing minimum, a subscribe-before-handshake order, an acknowledged-write requirement, an init quirk), record it in the contract and implement it from the citation. Hardware trial-and-error is for genuinely undocumented gaps only — never to re-derive a fact a reference already states.
- Treat scan/inspect/read/subscribe evidence as identity and telemetry discovery unless it is paired with mapping evidence.
- If no mapping source is found, generate a read/status/capabilities-only CLI or stop and ask the user for mapping evidence. Do not create callable write commands from raw GATT shape alone.
- Do not brute-force, fuzz, or actively probe mutating payloads on a physical device.
Protocol Contract
The device spec carries three views of "how this device works", alongside the action map in capabilities:
`transport:` — the quantitative contract. Fields the generator consumes or the codec needs:
write_mode: acknowledged | without-response— acknowledged (default) confirms each write before the next, so a control command is not dropped by an immediate disconnect. The generator emits the matching write path.command_spacing_ms: <int>— minimum time between writes. The firmware drops a command sent sooner than this after the previous one, so a burst loses all but the first. When set, the generator emits a paced writer that sleeps the deficit before every write — you do not hand-author pacing.connect_ceremony: [{name, characteristic_uuid, value_hex, wait_ms}]— the post-subscribe handshake some devices need before they accept control or stream telemetry. Codec/choreography input today; codegen is on the roadmap.settle_delays: [{name, ms}]— required pauses between state changes (e.g. a mode switch before a start).poll_cadence_ms: <int>— keep-alive / telemetry poll cadence.teardown: keep-running | stop-on-disconnect— does dropping the connection stop in-flight actuation?single_client: <bool>— only one BLE client at a time (the vendor app may need to disconnect first).
`quirks:` — the qualitative contract. Behavioral facts that do not reduce to a field but must be factored in: an init trick, a stale-session gotcha, a firmware-variant opcode shift, a notify-enable dance. Each is {category, summary, handling, evidence_refs}. They cannot drive codegen, so the generated CLI surfaces them in doctor (text + JSON); they are required reading for the codec author and a line on the dogfood checklist.
`workflows:` — the proven spine. The ordered, end-to-end sequences that operate the device for each user goal — start a walk, stop the belt, pair-then-arm — composing the transport: facts and the capabilities action map into the steps a working reference is known to use. Each is {name, goal, steps, evidence_refs, notes} with ordered, human-readable steps. It does not drive codegen; the generated CLI surfaces it in doctor (text + JSON), and it is the contract the implemented control flow (codec + held-connection choreography) is checked against in the QA workflow-fidelity pass (see below) and a line on the dogfood checklist. Capturing the spine is what keeps a from-scratch implementation from rediscovering a documented sequence by guessing.
Capture every transport field, quirk, and workflow you have evidence for, and cite each. The contract and the spine are the synthesis output — and they are your dogfood checklist: each entry is something to confirm on hardware, not rediscover. A divergence between the recorded contract/spine and observed behavior is a spec correction, not a fresh discovery.
Standalone Hardware Probe
Use cli-printing-press device-sniff ble when you need real-device evidence without printing a full CLI.
Use the standalone ble-probe binary only as a portable diagnostic fallback for machines that do not have the full Printing Press binary.
Build a live probe:
scripts/build-ble-probe.sh liveBuild a copyable Windows probe from macOS:
scripts/build-ble-probe.sh live --target windows/amd64Check the artifact before hardware work:
cli-printing-press device-sniff ble doctorFor a copied standalone artifact:
dist/ble-probe/live/$(go env GOOS)-$(go env GOARCH)/ble-probe doctorRun non-actuating evidence capture first:
cli-printing-press device-sniff ble scan --live --duration-ms 10000 > scan.json
cli-printing-press device-sniff ble inspect --live --address ADDRESS > inspect.json
cli-printing-press device-sniff ble read --live --address ADDRESS --service SERVICE_UUID --characteristic CHARACTERISTIC_UUID > read.json
cli-printing-press device-sniff ble subscribe --live --address ADDRESS --service SERVICE_UUID --characteristic CHARACTERISTIC_UUID --duration-ms 10000 > notify.jsonUse write only when a payload is known from observed evidence, docs, or a community protocol reference:
cli-printing-press device-sniff ble write --live --address ADDRESS --service SERVICE_UUID --characteristic CHARACTERISTIC_UUID --value-hex PAYLOAD_HEX > write.jsonMerge capture pieces before analysis:
cli-printing-press device-sniff ble merge --redact-term PERSONAL_TERM scan.json inspect.json read.json notify.json write.json > evidence.jsonSee docs/BLE-PROBE.md for macOS and Windows copy/run details.
Device Sniff Command
Given normalized evidence:
cli-printing-press device-sniff ble \
--input evidence.json \
--output "$RESEARCH_DIR/<device>-device.yaml" \
--analysis-output "$DISCOVERY_DIR/ble-analysis.json" \
--evidence-output "$DISCOVERY_DIR/ble-evidence-redacted.json" \
--redact-term PERSONAL_TERM \
--jsonThe alias must produce the same backend result:
cli-printing-press bluetooth-sniff \
--input evidence.json \
--output "$RESEARCH_DIR/<device>-device.yaml"Generate from the device spec:
cli-printing-press generate --spec "$RESEARCH_DIR/<device>-device.yaml" --validateSafety Stance
Safety labels are classification and provenance, not moral policing.
- Label accurately:
read-only,low-risk-write,physical-effect,configuration-risk, orunknown. - If unsure, use
unknown. - Observed or replay-validated commands can be generated even when labeled
physical-effect. - Unknown or insufficiently validated commands should stay metadata-only until stronger evidence exists.
- Physical-effect and configuration-risk writes should require dry-run preview or an explicit confirmation flag before non-verify replay.
- Normal verify/dogfood must not actuate real hardware. Verify-mode no-ops are expected for physical-effect writes.
- MCP read-only annotations must be conservative. False read-only is a bug; missing read-only is just a permission prompt.
Generated Live Control (Tier-1 vs Tier-2)
The generated CLI is replay-backed by default: commands echo the captured payload they would send and status reports the telemetry shape, without opening a connection. This default build is pure-Go, CGO-free, and never touches hardware (verify/dogfood/go test stay safe). Real control is opt-in on two axes:
- Build tag
-tags ble_livelinks the real BLE adapter (go build -tags ble_live ./...; CGO/CoreBluetooth on macOS, pure-Go D-Bus on Linux, WinRT on Windows). - Runtime flag
--live(with optional--address,--timeout) actuates the device. Physical-effect/configuration-risk commands still require--confirm-physical-effect; verify mode short-circuits before dialing.
Before shipping, classify the device and act accordingly:
Tier-1 — fixed-payload commands + readable telemetry. Works end to end with zero hand-authoring. The generated LiveTransport scans by service UUID, connects, writes each command's captured payload to its characteristic, and reads readable telemetry characteristics. Nothing to implement — verify go build -tags ble_live ./... compiles and (if you have hardware) --live actuates.
Tier-2 — stateful or parameterized protocols. A device whose commands need computed framing (checksums, sequence counters), value scaling (e.g. km/h → protocol units), notify-based telemetry frames, or a held-connection poll loop cannot be driven from static captured evidence. The generic transport will write the raw captured bytes, which is usually wrong for these devices. You MUST, in operator-owned files preserved across regeneration:
1. Write a codec (pure Go, no BLE) implementing device.DeviceCodec — EncodeCommand(command, args) builds a command's payload (using its positional CLI args for parameterized commands) and DecodeTelemetry(field, raw) turns a telemetry frame into a typed value. This is the single source of truth for the wire format, grounded in the mapping evidence and cited protocol references. Add a codec_test.go that tests it with no hardware. Register it from an init (codec = myCodec{}). 2. Declare parameterized commands in the device spec (commands[].parameters: [{name, type}]). The generator then emits the command with its <arg> usage, exact-arg validation, safety gating, dry-run, and verify no-op, and routes the args to your EncodeCommand. You do not hand-author the cobra command — only the encode logic. (A parameterized command with no registered codec is a hard error, not a silent static write.) 3. For stateful choreography only (hold a connection and poll, multi-step sequences), add a hand-authored command via the novelCommands hook (set it from an init in your own file in internal/cli; preserved as a NOVEL file across regen). Use the exported device.Dial(ctx, address, timeout) (device.Link, error) + device.Link (Write/Read/Subscribe/Close) with your codec — do not reimplement the BLE backend. Drive the choreography from the transport: contract: the connect ceremony, ordering (subscribe before handshake), settle delays, and poll cadence are recorded there, not re-derived. Command spacing and write mode are already handled — when transport.command_spacing_ms is set the generated device.Link.Write paces every write, and transport.write_mode selects the write path. Do not re-implement a paced-writer wrapper. 4. Gate every hand-authored live command on cliutil.IsVerifyEnv() (no-op under verify; device.Dial also refuses with ErrVerifyMode as a backstop) and cliutil.IsDogfoodEnv() (bound long-running work). Generated commands already carry this gating. Keep the print-by-default / --live-to-actuate stance and conservative MCP annotations. 5. Verify before ship: go test ./... (covers the codec and the generated Tier-1 path), go build -tags ble_live ./... compiles, and the live commands actuate on hardware when available.
A printed Tier-2 CLI whose live commands silently no-op or write wrong bytes is a failure, not an acceptable outcome — detect the stateful protocol and implement the codec. The reverse-engineered protocol logic is irreducible per-device work; the command surface, BLE plumbing, connection management, build tags, flags, and safety gating are generated.
QA: Workflow Fidelity
Before ship — once the codec and any held-connection choreography exist, and again during dogfood — diff the implemented control flow against each workflows: spine. <cli> doctor --json surfaces the recorded workflows as the deterministic inventory; for each one, walk the implementation (the codec's EncodeCommand, the novel choreography command, the ordering, settle delays, and waits) and confirm every step is present, in order, with nothing invented. Resolve any missing, reordered, or fabricated step before ship.
Assuming the cited source is credible (which it is when corroborated by the research pass above), a divergence from the proven spine is the prime suspect — caught here rather than through haphazard hardware trial-and-error. This is judgment, not a mechanical gate, and it is not definitive: the source can be wrong or the device variant can differ, which is exactly what hardware dogfood is for. But "does this follow the proven sequence?" beats guessing. A divergence the agent can justify (a documented better path, a known variant difference) is a spec note, not a silent deviation.
Session And UI Considerations
Session scaffolding is optional and device-driven.
- Omit session support for one-shot read/status devices.
- Emit session support when the spec declares low-latency repeated controls, notification streaming, telemetry sampling, reconnect sensitivity, or unreliable one-shot writes.
- Treat the generated session endpoint as local user-scoped control infrastructure for CLI commands and possible future UI apps, not as a public network API.
- For devices that allow only one client, document that the phone app may need to disconnect before laptop control.
Evidence Order
Prefer this sequence for unknown devices:
1. Identify the exact product, app, advertised BLE name, and service UUIDs. 2. Run an external web-research pass for protocol sources (docs, community code, HA / ESPHome, issues, forums, app logs, captures), expanding from any user-provided seeds rather than stopping at them; record the sources consulted and whether ≥2 are independent. 3. Synthesize the action map (capabilities), the operational contract (transport: + quirks:), and the proven workflows (workflows:) from the best sources, cross-checked and cited. 4. Scan and inspect to confirm identity and available characteristics. 5. Read and subscribe for non-mutating telemetry evidence. 6. Correlate observed writes with an action journal or imported capture. 7. Replay known payloads under operator-visible control. 8. Dogfood to verify the contract and spine on hardware — confirm each transport: field and quirk: holds, and that the implemented control flow follows each workflow: (the QA workflow-fidelity pass); a divergence is a spec correction, not a fresh discovery.
Do not actively probe mutating payloads without guidance.
Dogfood Testing Reference
Supplementary reference for Phase 5. The test protocol (steps 1-4, test
lists, reporting format) is inline in the main skill. This file contains
additional guidance: common failure patterns and what NOT to test.
Common Failure Patterns
| Symptom | Likely cause | Fix location |
|---|---|---|
| All list commands return empty | Response envelope not unwrapped | Client or output helpers |
--select strips everything | filterFields can't parse envelope | Add extractResponseData call |
--csv shows JSON | CSV check after JSON pipe check | Promoted template output path |
search returns no results | FTS table not wired into search cmd | search.go switch statement |
sync gets 404 on some endpoints | API version header mismatch | Client header per-path |
| Mutation command requires ugly name | operationId not cleaned up | Command Use: field |
<cmd> --help shows wrong example | Example field has placeholder values | Command Example: field |
me shows "0 results" | Provenance counter assumes array | Count single objects as 1 |
| Cancel/confirm path not discoverable | Subcommand buried under operationId group | Check bookings --help for Available Commands |
What NOT to Test
- Internal implementation details (store schema, migration order)
- Performance benchmarks
- Concurrent access
- Edge cases that require specific account setup (team features, org hierarchy)
- Endpoints the user doesn't have access to (org-level when user is individual)
Full Dogfood Confirmation
When the user selects "Full dogfood", confirm before creating test data:
"I'll create test data on your account (test bookings, test records, etc.)
and clean up by cancelling/deleting them. OK to proceed?"
Raw Documentation Fetching
When to read: Before Phase 1 docs research, Phase 1.5 absorb reads, or any step that extracts contract details from documentation or source pages.
Use fetch-docs.sh for fidelity-sensitive reads. The helper captures the raw response body with curl, preserves the final status code, caches successful reads for 24 hours, and returns the local file path for direct Read/Grep inspection.
When To Use It
Use the helper for:
- official API docs, auth guides, rate limits, error handling, pagination, webhooks, and per-endpoint reference pages
- OpenAPI, Postman, AsyncAPI, JSON Schema, YAML, markdown, and raw source links
- GitHub source files or plugin indexes where exact identifiers, enum values, header names, paths, and casing matter
- re-checking any docs URL that
WebFetchreported as missing, vague, or contradictory
Reserve WebFetch for quick TL;DR reads of blog posts, community articles, or pages where exact field-level contract details are not being carried into the generated CLI.
Command
skills/printing-press/references/fetch-docs.sh [--ttl=<seconds>] [--force] [--md] <url>Default behavior:
- follows redirects and compressed responses
- fails loudly on any final HTTP status other than
200 - writes captures under
${TMPDIR:-/tmp}/printing-press-fetch-docs/ - prints
path=,status=,content_type=, andeffective_url= - reuses a successful capture for 24 hours unless
--forceor--ttl=0is set
Use --force or --ttl=0 when validating route quirks such as trailing-slash 404 vs non-trailing-slash 200.
Use --md only when reader-mode markdown is helpful. If readability-cli and turndown-cli are installed, the helper converts HTML to markdown; otherwise it tries npx --yes for those tools. If conversion is unavailable or fails, it keeps the raw capture and prints a warning. For navigation discovery, raw HTML is usually better than reader-mode markdown because nav links are part of the contract surface.
Required Agent Handling
After fetching:
1. Read or grep the returned path= file directly. 2. Record non-200 statuses as reachability evidence instead of summarizing them away. 3. When two URL variants differ only by trailing slash, capitalization, .html, or a docs-build quirk, fetch both variants and preserve both status codes in the brief. 4. Copy exact enum values, header names, field names, status codes, path casing, and examples from the raw capture when they affect spec authoring or CLI flags. 5. Do not base generator-contract decisions on WebFetch summaries when the raw capture is available.
#!/usr/bin/env bash
# fetch-docs.sh - fetch fidelity-sensitive docs without a summarization layer.
#
# Usage:
# fetch-docs.sh [--ttl=<seconds>] [--force] [--md] <url>
#
# Prints the cached/captured file path and response metadata. Exits non-zero on
# network failure or any final HTTP status other than 200.
set -euo pipefail
usage() {
echo "usage: $0 [--ttl=<seconds>] [--force] [--md] <url>" >&2
exit 2
}
TTL_SECONDS=86400
FORCE=false
WANT_MD=false
while [[ $# -gt 0 ]]; do
case "$1" in
--ttl=*)
TTL_SECONDS="${1#--ttl=}"
[[ "$TTL_SECONDS" =~ ^[0-9]+$ ]] || usage
shift
;;
--ttl)
TTL_SECONDS="${2:-}"
[[ "$TTL_SECONDS" =~ ^[0-9]+$ ]] || usage
shift 2
;;
--force)
FORCE=true
TTL_SECONDS=0
shift
;;
--md)
WANT_MD=true
shift
;;
-h|--help)
usage
;;
--*)
usage
;;
*)
break
;;
esac
done
[[ $# -eq 1 ]] || usage
URL="$1"
case "$URL" in
http://*|https://*) ;;
*) echo "fetch-docs: URL must start with http:// or https://" >&2; exit 2 ;;
esac
if ! command -v curl >/dev/null 2>&1; then
echo "fetch-docs: curl is required" >&2
exit 127
fi
CACHE_ROOT="${TMPDIR:-/tmp}/printing-press-fetch-docs"
mkdir -p "$CACHE_ROOT"
if command -v shasum >/dev/null 2>&1; then
URL_HASH="$(printf '%s' "$URL" | shasum -a 256 | awk '{print $1}')"
else
URL_HASH="$(printf '%s' "$URL" | cksum | awk '{print $1}')"
fi
safe_slug() {
printf '%s' "$1" |
sed -E 's%^[a-zA-Z][a-zA-Z0-9+.-]*://%%; s%[/?#&=]+%-%g; s%[^A-Za-z0-9._-]+%-%g; s%^-+%%; s%-+$%%' |
cut -c1-80
}
extension_for() {
local url="$1"
local content_type="$2"
local path="${url%%\?*}"
path="${path%%#*}"
case "$path" in
*.md|*.markdown) echo "md"; return ;;
*.mdx) echo "mdx"; return ;;
*.json) echo "json"; return ;;
*.yaml|*.yml) echo "yaml"; return ;;
*.txt) echo "txt"; return ;;
*.xml) echo "xml"; return ;;
*.html|*.htm) echo "html"; return ;;
esac
case "$content_type" in
*markdown*) echo "md" ;;
*json*) echo "json" ;;
*yaml*|*yml*) echo "yaml" ;;
*xml*) echo "xml" ;;
*html*) echo "html" ;;
*text/plain*) echo "txt" ;;
*) echo "bin" ;;
esac
}
SLUG="$(safe_slug "$URL")"
[[ -n "$SLUG" ]] || SLUG="docs"
BASE="$CACHE_ROOT/${SLUG}-${URL_HASH:0:16}"
META_FILE="$BASE.meta"
cache_fresh() {
local file="$1"
[[ -f "$file" && -f "$META_FILE" ]] || return 1
[[ "$FORCE" == "false" ]] || return 1
[[ "$TTL_SECONDS" -gt 0 ]] || return 1
local now mtime age
now="$(date +%s)"
if mtime="$(stat -f %m "$file" 2>/dev/null)"; then
:
else
mtime="$(stat -c %Y "$file" 2>/dev/null)" || return 1
fi
age=$((now - mtime))
[[ "$age" -lt "$TTL_SECONDS" ]]
}
print_result() {
local file="$1"
local status="$2"
local content_type="$3"
local effective_url="$4"
printf 'path=%s\n' "$file"
printf 'status=%s\n' "$status"
printf 'content_type=%s\n' "$content_type"
printf 'effective_url=%s\n' "$effective_url"
}
cached_file=""
if [[ -f "$META_FILE" ]]; then
cached_file="$(awk -F= '$1 == "path" {print substr($0, index($0, "=") + 1)}' "$META_FILE" 2>/dev/null || true)"
if [[ -n "$cached_file" ]] && cache_fresh "$cached_file"; then
status="$(awk -F= '$1 == "status" {print substr($0, index($0, "=") + 1)}' "$META_FILE")"
content_type="$(awk -F= '$1 == "content_type" {print substr($0, index($0, "=") + 1)}' "$META_FILE")"
effective_url="$(awk -F= '$1 == "effective_url" {print substr($0, index($0, "=") + 1)}' "$META_FILE")"
if [[ "$status" != "200" ]]; then
echo "fetch-docs: cached HTTP $status for $URL (body: $cached_file, final: $effective_url)" >&2
exit 22
fi
print_result "$cached_file" "$status" "$content_type" "$effective_url"
exit 0
fi
fi
BODY_TMP="$(mktemp "$CACHE_ROOT/fetch-docs-body.XXXXXX")"
HEADER_TMP="$(mktemp "$CACHE_ROOT/fetch-docs-headers.XXXXXX")"
trap 'rm -f ${BODY_TMP:+"$BODY_TMP"} "$HEADER_TMP"' EXIT
CURL_WRITE=$'status=%{http_code}\ncontent_type=%{content_type}\neffective_url=%{url_effective}\n'
if ! CURL_META="$(curl -sS -L --compressed --connect-timeout 10 --max-time 30 \
-D "$HEADER_TMP" -o "$BODY_TMP" -w "$CURL_WRITE" "$URL" 2>&1)"; then
echo "fetch-docs: curl failed for $URL" >&2
printf '%s\n' "$CURL_META" >&2
exit 1
fi
status="$(printf '%s\n' "$CURL_META" | awk -F= '$1 == "status" {print substr($0, index($0, "=") + 1)}')"
content_type="$(printf '%s\n' "$CURL_META" | awk -F= '$1 == "content_type" {print substr($0, index($0, "=") + 1)}')"
effective_url="$(printf '%s\n' "$CURL_META" | awk -F= '$1 == "effective_url" {print substr($0, index($0, "=") + 1)}')"
[[ -n "$status" ]] || status="000"
ext="$(extension_for "$effective_url" "$content_type")"
RAW_FILE="$BASE.$ext"
mv "$BODY_TMP" "$RAW_FILE"
BODY_TMP=""
if [[ "$status" != "200" ]]; then
{
printf 'path=%s\n' "$RAW_FILE"
printf 'status=%s\n' "$status"
printf 'content_type=%s\n' "$content_type"
printf 'effective_url=%s\n' "$effective_url"
} > "$META_FILE"
echo "fetch-docs: HTTP $status for $URL (body: $RAW_FILE, final: $effective_url)" >&2
exit 22
fi
OUT_FILE="$RAW_FILE"
if [[ "$WANT_MD" == "true" && "$ext" != "md" && "$ext" != "mdx" ]]; then
MD_FILE="$BASE.md"
if command -v readability-cli >/dev/null 2>&1 && command -v turndown-cli >/dev/null 2>&1; then
if readability-cli "$effective_url" < "$RAW_FILE" | turndown-cli > "$MD_FILE"; then
OUT_FILE="$MD_FILE"
content_type="text/markdown; converted-from=$content_type"
else
echo "fetch-docs: markdown conversion failed; keeping raw capture $RAW_FILE" >&2
rm -f "$MD_FILE"
fi
elif command -v npx >/dev/null 2>&1; then
if npx --yes readability-cli "$effective_url" < "$RAW_FILE" | npx --yes turndown-cli > "$MD_FILE"; then
OUT_FILE="$MD_FILE"
content_type="text/markdown; converted-from=$content_type"
else
echo "fetch-docs: npx markdown conversion failed; keeping raw capture $RAW_FILE" >&2
rm -f "$MD_FILE"
fi
else
echo "fetch-docs: --md requested but readability-cli/turndown-cli or npx are unavailable; keeping raw capture $RAW_FILE" >&2
fi
fi
{
printf 'path=%s\n' "$OUT_FILE"
printf 'status=%s\n' "$status"
printf 'content_type=%s\n' "$content_type"
printf 'effective_url=%s\n' "$effective_url"
} > "$META_FILE"
print_result "$OUT_FILE" "$status" "$content_type" "$effective_url"
Known OpenAPI Specs
APIs with verified, publicly accessible OpenAPI specifications. The printing-press skill checks this registry before searching the web.
How to use
1. Search this file for the API name 2. If found, download the spec URL directly 3. If not found, proceed to web search
Registry
| API | Spec URL | Format | OpenAPI Version | Verified |
|---|---|---|---|---|
| Petstore | https://petstore3.swagger.io/api/v3/openapi.yaml | YAML | 3.0.x | Yes |
| Stytch | https://raw.githubusercontent.com/stytchauth/stytch-openapi/main/openapi.yml | YAML | 3.0.x | Yes |
| Discord | https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json | JSON | 3.1.0 | Yes |
| Stripe | https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json | JSON | 3.0.x | Yes |
| Twilio | https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json | JSON | 3.0.x | Yes |
| SendGrid | https://raw.githubusercontent.com/twilio/sendgrid-oai/main/spec/json/sendgrid_oai.json | JSON | 3.0.x | Yes |
| GitHub | https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2022-11-28.yaml | YAML | 3.0.x | Yes |
| GitLab | Skipped (no stable public raw OpenAPI spec URL confirmed) | N/A | N/A | No |
| DigitalOcean | https://raw.githubusercontent.com/digitalocean/openapi/main/specification/DigitalOcean-public.v2.yaml | YAML | 3.0.x | Yes |
| Asana | https://raw.githubusercontent.com/Asana/openapi/main/defs/asana_oas.yaml | YAML | 3.0.x | Yes |
| Square | https://raw.githubusercontent.com/square/square-openapi/master/openapi.json | JSON | 3.0.x | Yes |
| Notion | Skipped (official public OpenAPI spec URL not confirmed) | N/A | N/A | No |
| Linear | https://raw.githubusercontent.com/linear/linear/master/packages/sdk/src/schema.graphql | GraphQL SDL | N/A | Yes |
| HubSpot | https://raw.githubusercontent.com/HubSpot/HubSpot-public-api-spec-collection/main/PublicApiSpecs/CRM/Contacts/Rollouts/424/v3/contacts.json | JSON | 3.0.x | Yes |
| Front | https://raw.githubusercontent.com/frontapp/front-api-specs/main/core-api/core-api.json | JSON | 3.0.x | Yes |
Notes
- All URLs are raw file URLs (not HTML pages)
- Verified means the URL was confirmed accessible and the spec parses correctly
- Some specs are very large (Discord: 1MB+) - the parser handles this with resource/endpoint limits
- This environment blocks direct shell network egress (
curl -sIreturns000), so URL checks were completed via repository/raw URL validation instead of localcurl
Non-Obvious Insight (NOI) Formula
Formula
[API] isn't just [Obvious Frame]. It's [Insight Frame]. [Signal Sentence].
The NOI reframes what an API does from its surface-level description into a deeper insight about how power users actually leverage it. This drives CLI feature selection toward workflows that matter rather than CRUD wrappers.
Examples by Domain Archetype
Project Management
1. Linear: Linear isn't just an issue tracker. It's a velocity sensor for engineering teams. Every sprint boundary reveals whether a team is accelerating or decelerating before managers notice.
2. Jira: Jira isn't just a ticket system. It's an organizational memory of every decision and tradeoff. The comment threads on old tickets contain the rationale that onboarding docs never capture.
3. Asana: Asana isn't just a task manager. It's a dependency graph that exposes bottlenecks. When three projects share the same blocked task, it reveals the single person everyone is waiting on.
4. GitHub Issues: GitHub Issues isn't just a bug tracker. It's a public roadmap signal. The ratio of feature requests to bug reports tells you whether users want more or want better.
Communication
5. Slack: Slack isn't just a messaging app. It's an attention allocator. The channels with the highest unread counts reveal the topics your organization cares about but hasn't staffed.
6. Discord: Discord isn't just a chat platform. It's a community health monitor. Thread reply depth and reaction diversity predict whether a community is growing or hollowing out.
7. Twilio: Twilio isn't just an SMS API. It's a delivery reliability surface. The gap between sent and delivered timestamps exposes carrier routing problems before customers complain.
Payments
8. Stripe: Stripe isn't just a payment processor. It's a revenue pattern detector. Subscription churn clusters around specific billing cycle days, revealing pricing sensitivity windows.
9. Square: Square isn't just a POS system. It's a foot traffic sensor. Transaction timestamps and amounts map customer behavior patterns that no survey captures.
10. Plaid: Plaid isn't just a banking connector. It's a financial behavior classifier. Transaction categorization patterns reveal spending personalities that credit scores miss.
Infrastructure
11. AWS EC2: EC2 isn't just a compute service. It's a capacity planning oracle. Instance type migration patterns across an org reveal which workloads are growing before the bills spike.
12. Cloudflare: Cloudflare isn't just a CDN. It's a traffic intent classifier. The ratio of cached to uncached requests reveals whether your users are browsing or building.
Content
13. Notion: Notion isn't just a wiki. It's a knowledge decay detector. Pages with high view counts but stale edit dates are the documentation traps that cause the most onboarding friction.
14. WordPress: WordPress isn't just a CMS. It's a content performance lab. The correlation between publish timing, word count, and engagement reveals the format your audience actually wants.
Novel Features Subagent
When to read: This file is referenced by Phase 1.5 Step 1.5c.5 of the
printing-press skill. It owns the input bundle contract, the Task subagent
invocation, the subagent prompt template, and the output parsing rules for
novel-feature brainstorming.
Invariants
The subagent spawns once per absorb step, in every cli-printing-press run. The only legal non-spawn outcome is the pre-flight HALT defined below.
- A missing prior
research.jsonis a first print, not a skip signal — the
subagent still spawns; only Pass 2(d) (reprint reconciliation) is omitted.
- A complete-looking prior manifest is not a skip signal either — Pass 2(d)
re-scores prior features against the current personas, which is the entire reason the reprint case exists. Strong priors make the spawn more valuable, not less.
- Hand-curating from a prior manifest, falling back to inline brainstorming
in SKILL.md, or disclosing a skip in the gate showcase are violations of this contract, not exits from it.
- Run the prior-research discovery snippet below as written. Do not
substitute a hand-eyeballed ls of the manuscripts directory; the snippet's path ($PRESS_MANUSCRIPTS/<api-slug>/*/research.json) is at the run-id level, not inside the research/ subdirectory.
Pre-flight check
Before spawning, confirm the Phase 1 brief has both:
- a populated
Userssection with concrete user types, AND - a populated
Top Workflowssection with named rituals.
If either is missing or pro-forma ("developers", "general users", "anyone using the API"), do NOT spawn the subagent. The brainstorm will fabricate plausible personas instead of grounding in research. Loop back to Phase 1 user research and return here.
Input bundle
Assemble these values before spawning. Substitute into the prompt template below.
| Variable | Value |
|---|---|
${BRIEF_PATH} | $RESEARCH_DIR/<stamp>-feat-<api>-pp-cli-brief.md — also embeds DeepWiki findings (## Codebase Intelligence) and user briefing (## User Vision) when present |
${ABSORB_MANIFEST_CONTENT} | Inline markdown: the absorbed-features table built in Step 1.5b. The on-disk manifest at <stamp>-feat-<api>-pp-cli-absorb-manifest.md is not written until Step 1.5d, so paste the table content directly into the prompt. |
${RUBRIC_PATH} | Absolute path to references/absorb-scoring.md (the scoring rubric) |
${API_SPEC_SUMMARY} | Inline 1-paragraph summary: API surface (endpoint families), auth shape, NOI domain |
${PRIOR_RESEARCH_PATH} | Result of the prior-research discovery snippet below, or the literal string none |
Prior-research discovery. Run before assembling the bundle to set ${PRIOR_RESEARCH_PATH}:
PRIOR_RESEARCH=none
if [ -f "$PRESS_LIBRARY/<api>/research.json" ]; then
PRIOR_RESEARCH="$PRESS_LIBRARY/<api>/research.json"
elif [ -d "$PRESS_MANUSCRIPTS/<api-slug>" ]; then
CANDIDATE=$(ls -1t "$PRESS_MANUSCRIPTS/<api-slug>"/*/research.json 2>/dev/null | head -1)
[ -n "$CANDIDATE" ] && PRIOR_RESEARCH="$CANDIDATE"
fiUser vision and DeepWiki findings are not separate variables — they live inside the brief. The subagent detects their presence by checking for the ## User Vision and ## Codebase Intelligence sections in ${BRIEF_PATH}.
Subagent invocation
One Task tool call. Do not split passes across multiple invocations — the cut pass must see the candidates it generated.
Agent({
description: "Novel-features brainstorm + adversarial cut",
subagent_type: "general-purpose",
prompt: <Subagent prompt template below, with ${...} placeholders substituted>
})Subagent prompt template
```` You are a novel-features brainstorming agent for the Printing Press, a system that generates CLIs for APIs. Your job is to propose the differentiating features for ONE printed CLI and then ruthlessly cut the weak ones.
You will run THREE PASSES IN ORDER. The third pass is not optional and is not a polish pass — it is the cut.
Inputs (read these before Pass 1)
- Brief (read from disk): ${BRIEF_PATH}
- Scoring rubric (read): ${RUBRIC_PATH} (kill/keep checks, score dimensions, table format, reprint verdicts)
- Prior research.json: ${PRIOR_RESEARCH_PATH} (read from disk if real path; literal
nonemeans first print) - API spec summary: ${API_SPEC_SUMMARY} (inline)
Inline absorb manifest (features ALREADY covered — do not re-propose):
${ABSORB_MANIFEST_CONTENT}
The brief may contain ## User Vision and ## Codebase Intelligence sections. Detect their presence after reading the brief; they gate Pass 2 sources (e) and (f).
If ${PRIOR_RESEARCH_PATH} is a real path, this is a REPRINT and Pass 2 step (d) is mandatory.
Pass 1: Customer pass
Write down WHO the user of this CLI is BEFORE proposing any features. Output under heading ## Customer model.
- 2-4 named personas. NOT "developers" or "users" — concrete people with
concrete habits drawn from the brief's Users + Top Workflows sections. Good: "Someone who checks HN every morning before standup." Good: "A hiring manager scanning Who's Hiring threads monthly." Bad: "API users."
- For each persona, write three short paragraphs:
- Today (without this CLI): What do they do today to accomplish their
weekly ritual with this API? What tabs do they have open? What scripts do they re-run? What questions can they NOT answer?
- Weekly ritual: The repeated workflow that defines their relationship
with this service.
- Frustration: The single most tedious or impossible part of the ritual.
If you cannot do this from the brief alone — if you find yourself inventing personas to keep going — STOP and return ONLY:
## Failure: brief lacks user research
The Phase 1 brief does not contain enough Users / Top Workflows detail to
ground a customer model. Re-run Phase 1 user research before spawning this
subagent again.Do not proceed to Pass 2 with fabricated personas.
Pass 2: Generate pass
Output under heading ## Candidates (pre-cut). Generate ~2× the features you expect to ship — aim for 8-16 candidates total.
Sources for candidates (label each candidate with its source):
(a) Persona-driven: For each persona's frustration from Pass 1, propose the one-command feature that resolves it.
(b) Service-specific content patterns: What unique content types or workflows define this service's identity (e.g., HN's Show HN / Ask HN / Who's Hiring; Spotify's Discover Weekly / Wrapped; GitHub's Discussions; TMDb's Collections / Watch Providers)? Propose features that exploit each.
(c) Cross-entity local queries: What joins across synced tables produce insights no single API call can?
(d) Reprint reconciliation (only if ${PRIOR_RESEARCH_PATH} is a real path): Load the prior novel_features and novel_features_built arrays. For each prior feature, score it against the current personas using the rubric. Add to candidates with verdict tag prior-keep, prior-reframe, or prior-drop. The prior list is candidate input, not gospel and not noise.
(e) User briefing (only if the brief contains a ## User Vision section): Propose features that directly serve the user's stated vision but are not already covered by the absorb manifest.
(f) DeepWiki (only if the brief contains a ## Codebase Intelligence section): Propose features that exploit internal data relationships, queue/worker patterns, or event systems that the public API docs don't suggest.
For each candidate capture: name, command, one-line description, persona served, source label from the list above, and Long Description (none unless the overlap rule below applies).
When 2+ compound-tool candidates could plausibly match the same natural-language request because their input surfaces overlap (for example, user UPN, market ID, customer email, or recipe name), plan each overlapping command's Long: description with an explicit scope redirect:
Use this command for <matching intent>.
Do NOT use this command for <nearby but wrong intent>; use '<sibling-command>' instead.Only skip this pattern for single-tool CLIs or candidates whose inputs and operator intent do not overlap with sibling tools. A slightly longer Long: description is acceptable when it prevents an agent from choosing the wrong compound tool.
Apply the rubric's kill/keep checks (LLM dependency, external service, auth gap, scope creep, verifiability, reimplementation) inline. Reframe or cut obvious failures NOW so they don't waste Pass 3 attention.
Framework command flags
The generated framework commands have stable flag vocabularies. Use these verbatim in narrative.quickstart, narrative.recipes, and any example that calls a framework command:
sync:--resources <csv>,--since <duration like 7d/24h/1w/30m>,--full,--latest-only,--max-pages <int>,--param key=value,--resource-param resource:key=value,--global-param key=value,--db <path>search:--type <single resource>,--limit <int>,--db <path>analytics:--type <single resource>,--group-by <field>,--limit <int>,--db <path>tail:--resource <single resource>,--interval <duration>,--since <timestamp>,--follow- global output flags on any command:
--json,--agent,--select <fields>,--compact,--quiet,--plain,--csv
Do not invent plausible variants such as --entities, --types, --query, or absolute dates for sync --since. If you need multiple resources, use sync --resources customers,charges. If you need search scoped to one resource, use search "term" --type customers. Do not use search --entities.
Hand-written novel commands that read from the local store must call the generated sync hint helpers before returning local results:
if !hintIfUnsynced(cmd, db, "<resource>") {
hintIfStale(cmd, db, "<resource>", flags.maxAge)
}Use "" for commands that scan all local resources. The helpers write only to stderr, so JSON/stdout output stays stable; --max-age 0 disables stale-read hints.
Every hand-written novel command must also declare its data-source strategy in the command source file:
// pp:data-source localUse exactly one of:
autofor commands that honor--data-source auto|local|liveby choosing
live data with local fallback.
localfor commands that only read synced/local SQLite data. These commands
must reject --data-source live with a clear "no live equivalent" error.
livefor commands that only call the remote API. These commands must reject
--data-source local with a clear "no local data source" error.
Dogfood reports hand-written novel commands that omit the annotation or use an unknown strategy.
Pass 3: Adversarial cut pass
This is the pass that exists because brainstorms without it produce flabby lists. Output under heading ## Survivors and kills.
For EVERY surviving candidate, force-answer these in writing:
1. Weekly use: Would the named persona actually run this command at least weekly? "Monthly", "occasionally", or "depends" is a soft kill. 2. Wrapper vs leverage: Is this a thin renaming of one API endpoint the user could call directly with a generic client? If yes, kill it — wrappers do not justify transcendence rows. 3. Transcendence proof: Does this feature get its power from local SQLite, a cross-source join, agent-shaped output, or a service-specific content pattern? If the answer is "none of the above", it is not transcendent. 4. Sibling kill: Name the closest candidate you killed and why. If you cannot, you didn't generate enough candidates — return to Pass 2 and add more. 5. Buildability: Will the generator auto-emit this from the spec, or will the agent need to hand-write a Cobra file plus root.go wiring after generate? Tag exactly one value:
spec-emits— the feature reuses an endpoint already in the spec
and the generator's emit path (endpoint mirror or extra_commands) produces a working command without hand-edits.
hand-code— the feature requires SQLite joins, cross-source
synthesis, custom output shapes, or any Go code beyond what the generator emits today (~50-150 LoC per feature plus root.go wiring). This is the default for transcendence features; most candidates fall here. 6. Long-description validity: If the survivor has a planned Long Description, confirm every named sibling command still survives with that exact command name. If Pass 3 killed or renamed the sibling, revise the redirect to point at the surviving command, or clear it to none when no accurate sibling remains.
Drop ~half. Target output: 4-8 survivors. Score survivors with the rubric's 4-dimension score; only keep features scoring >= 5/10.
For every killed candidate, record a one-sentence kill reason. Killed candidates are PART of the output, not silent.
Output contract
Return a single markdown document with these top-level sections, in this order:
1. ## Customer model — personas from Pass 1. 2. ## Candidates (pre-cut) — full Pass 2 list with source labels, Long Description (none when not needed), and inline kill/keep verdicts from the rubric. 3. ## Survivors and kills
### Survivors— features scoring >= 5/10, formatted as a
transcendence table matching the rubric's "Transcendence Table Format" section (which includes the Buildability column, spec-emits or hand-code per Pass 3 question 5). Include score, persona-served, the one-sentence buildability proof per the rubric, the buildability tag, and Long Description (none when not needed). Any non-none Long Description MUST reference only surviving command names after Pass 3.
### Killed candidates— table with columns: feature, kill reason,
closest-surviving-sibling. 4. ## Reprint verdicts (REPRINT ONLY) — per-prior-feature: keep / reframe / drop, with one-line justification, per the rubric's reprint verdict rules.
Do not include any other sections. Do not summarize. Do not editorialize. Do not propose follow-up work. ````
Output handling in SKILL.md
After the subagent returns:
1. Parse `### Survivors` — these become the transcendence rows in the absorb manifest (Step 1.5d). The score, buildability proof, and Buildability tag flow into the transcendence table; the persona-served column is the audit trail. Preserve non-none Long Description values in the manifest row so Phase 3 hand-code uses them as the Cobra Long text instead of losing them in the brainstorm audit trail. The Buildability column drives Phase Gate 1.5's hand-code count: rows tagged hand-code are the agent's post-generate scope commitment, rows tagged spec-emits are not. 2. Parse `## Reprint verdicts` (if present) — record dropped prior features under the transcendence table per the rubric's reprint surface rule, so the user can override drops at the Phase 1.5 gate review. 3. Audit trail — save the full subagent response (including ## Customer model, ## Candidates (pre-cut), ### Killed candidates) to $RESEARCH_DIR/<stamp>-novel-features-brainstorm.md. The Customer model and Killed candidates do NOT go into the manifest, but they MUST be persisted for retro/dogfood debugging. 4. Failure handling — if the subagent returned the ## Failure: brief lacks user research envelope, HALT Phase 1.5 immediately. Do not fall back to inline brainstorming. Surface the failure to the user and re-run Phase 1 user research.
OAuth2 PKCE CLI Checklist
Use this checklist for printed CLI commands that obtain user-context tokens through an OAuth2 browser authorization flow. It applies to hand-written novel commands and to any future generator/template support for OAuth2 Authorization Code + PKCE.
Required behavior
- Generate cryptographically random
stateper login attempt. - Generate a cryptographically random PKCE verifier per login attempt.
- Use
code_challenge_method=S256and a URL-safe SHA-256code_challenge. - Validate callback
statebefore exchanging any authorization code. - Bind loopback callback listeners only to loopback hosts (
127.0.0.1,localhost, or::1). - Require the redirect URI to match the provider registration and the command's listener.
- Store access tokens, refresh tokens, scopes, and expiry metadata through the CLI's normal config/storage path.
- Keep cookie/browser-session login paths distinct from OAuth2 token acquisition paths.
Timeout handling
Use separate timeout contexts for separate phases:
1. Browser callback wait: long enough for the user to complete login/consent. 2. Token exchange: shorter network timeout after the callback has arrived.
Do not pass the browser-wait context into the token exchange. A user who authorizes near the callback deadline should not get an ambiguous token-exchange failure caused by leftover callback time.
Machine-output and browser-launch rules
OAuth login commands often have two ways to get the user to the authorize URL:
- open the browser automatically; or
- print the authorize URL for manual copy/paste.
Never suppress both paths.
If --json or another machine-output mode suppresses human-readable stdout, and --no-open disables browser launch, the command must either:
1. fail fast with an actionable usage error; or 2. implement an explicit machine protocol that emits structured data such as authorize_url, state_id, expires_at, and a documented continuation command.
Do not silently wait for a callback that cannot happen.
Under cliutil.IsVerifyEnv(), do not open browsers or dial OS handlers. Prefer a typed verify/dry-run result or a usage error that names the non-opening mode.
JSON cleanliness
- Do not print prose to stdout in
--jsonmode. - If warnings are needed in JSON mode, put them in the JSON envelope or write them to stderr only when that matches the CLI's existing error contract.
- Keep final success output machine-parseable and free of OAuth secrets.
Test requirements
Add focused tests for:
- authorize URL construction, including
state, scope, redirect URI, S256 challenge, and response type; - callback state mismatch;
- end-to-end loopback flow with a local token server;
- token storage metadata, including refresh-token presence, scopes, and expiry when applicable;
- browser-open failure or
--no-openURL printing behavior; - impossible mode combinations such as
--json --no-open; - timeout behavior for callback wait versus token exchange when practical.
Run go test -race for tests involving callback servers, token servers, channels, or goroutines.
Race-safety rules for tests
HTTP test handlers run in separate goroutines. Do not write a plain bool, map, slice, or struct field in an httptest.Server handler and read it from the main test goroutine without synchronization.
Use one of:
sync/atomic.Boolor another atomic type;- a channel;
- a mutex-protected struct.
A completed http.Client.Do call is not a general synchronization primitive for arbitrary server-side writes.
Review red flags
Treat these as blockers or at least high-priority review comments:
- plain
boolor unsynchronized state shared between HTTP handler goroutines and test goroutines; - the same context used for both browser callback wait and token exchange;
--jsonhides the authorize URL while--no-openprevents browser launch;- browser auto-open runs under
PRINTING_PRESS_VERIFY=1; - OAuth2 user-context tokens are conflated with app-only bearer tokens or cookie-capture login;
- callback accepts non-loopback redirect hosts without an explicit, reviewed reason;
- token responses are logged or printed in full;
- generated docs tell users to use an app-only bearer token for user-context writes or personal reads.
When to promote this into generator code
This checklist is enough for hand-written novel OAuth commands. If cli-printing-press starts generating OAuth2 PKCE login commands from specs/templates, promote these rules into the generator and golden tests so every printed CLI inherits them by construction.
Per-source rate limiting
Hand-written clients in sibling internal packages (internal/source/<name>/, internal/recipes/, internal/phgraphql/, etc.) MUST use cliutil.AdaptiveLimiter and surface *cliutil.RateLimitError from public methods when 429 retries are exhausted. Returning empty results on throttle is silent corruption: downstream queries cannot tell "the source has no data" from "the source rate-limited us."
Reference shape
import (
"context"
"errors"
"net/http"
"<modulePath>/internal/cliutil"
)
type Client struct {
HTTP *http.Client
limiter *cliutil.AdaptiveLimiter
}
func New() *Client {
return &Client{
HTTP: &http.Client{},
limiter: cliutil.NewAdaptiveLimiter(2.0),
}
}
func (c *Client) Fetch(ctx context.Context, url string) ([]byte, error) {
c.limiter.Wait()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := c.HTTP.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
c.limiter.OnRateLimit()
return nil, &cliutil.RateLimitError{
URL: url,
RetryAfter: cliutil.RetryAfter(resp),
}
}
c.limiter.OnSuccess()
// ... read + return body ...
return nil, nil
}Higher-level fanout commands
Aggregations that fan out N filings/items MUST errors.As(err, &rateErr) and propagate the failure rather than continue-ing past it:
for _, item := range items {
detail, err := c.FetchDetail(ctx, item.ID)
if err != nil {
var rateErr *cliutil.RateLimitError
if errors.As(err, &rateErr) {
return nil, err
}
continue // other errors are still skippable
}
out = append(out, detail)
}Enforcement
cli-printing-press dogfood runs source_client_check, which scans every internal/<pkg>/*.go file outside the generator-emitted set. A file that makes outbound HTTP calls but lacks a limiter signal or a typed-error signal produces a finding.
Steinberger Scorecard - Exact Scoring Patterns
This table maps each scorecard dimension to the exact file and string patterns measured by internal/pipeline/scorecard.go. Use this to target Phase 4 fixes at patterns that actually score points.
Scoring Algorithm
Each dimension is 0-10. Total is 0-90. Grade: A >= 80%, B >= 65%, C >= 50%, D >= 35%, F < 35%.
Run the scorecard: cli-printing-press scorecard --dir ./<api>-cli
Dimension Reference
| Dimension | File Checked | Patterns (points each) | Max |
|---|---|---|---|
| Output Modes | internal/cli/root.go | json(+2) plain(+2) select(+2) table(+2) csv(+2) | 10 |
| Auth | internal/config/config.go + file existence | os.Getenv count: 1 call = 5pts, 2+ calls = 8pts. internal/cli/auth.go exists = +2pts | 10 |
| Error Handling | internal/cli/helpers.go | hint: or Hint: present = +5. Count of code: occurrences (1pt each, max 5) | 10 |
| Terminal UX | internal/cli/helpers.go | colorEnabled(+5) NO_COLOR(+3) isatty(+2) | 10 |
| README | README.md | Exact section strings: Quick Start(+2) Output Formats(+2) Agent Usage(+2) Troubleshooting(+2) Doctor(+2) | 10 |
| Doctor | internal/cli/doctor.go | Count HTTP patterns x2: http.Get, http.Head, http.NewRequest, http.Client, httpClient.Get, httpClient.Head, httpClient.Do | 10 |
| Agent Native | internal/cli/root.go + helpers.go | json(+2) select(+2) dry-run/dryRun/dry_run(+2) non-interactive/nonInteractive(+1) stdin(+1) "yes"(+1) 409/already exists(+1) human-friendly/humanFriendly(+1) | 10 |
| Local Cache | internal/client/client.go + internal/cache/cache.go | cacheDir/readCache/writeCache(+5) no-cache/NoCache(+2) sqlite/bolt/badger in cache.go or store.go(+3) | 10 |
| Breadth | internal/cli/*.go file count | Excludes: helpers.go, root.go, doctor.go, auth.go. Thresholds: <5=0, 5-10=3, 11-20=5, 21-40=7, 41-60=9, 60+=10 | 10 |
| Workflows | internal/cli/*.go registered commands | Workflow filename prefixes, direct store use, package-local store helpers, or 2+ client API calls | 10 |
| Insight | internal/cli/*.go registered commands | Insight filename prefixes, store use plus aggregation patterns (COUNT(, SUM(, GROUP BY, rates), or manifest novel-feature hints | 10 |
Key Gotchas
- README scorecard checks for exact string
"Doctor"(capital D), not"Health Check"or"doctor" - Agent Native checks root.go + helpers.go ONLY -
stdinin command files doesn't count - Auth gives +2 just for
auth.goexisting as a file, regardless of content - Local Cache gives +3 for
sqlite/bolt/badgerstrings ininternal/cache/cache.goorinternal/store/store.go - Breadth counts files, not commands - one file per endpoint beats one file per resource
- Workflow scoring follows registered Cobra commands and recognizes package-local helpers that reach the store
Secret & PII Protection — Implementation Details
Read this file during Phase 5.5 (Archive Manuscripts) and before any publish step. The cardinal rules in SKILL.md apply at all times. This file has the implementation.
Exact-value scan before archiving
The skill knows the API key if the user provided one. Before archiving manuscripts, scan all artifacts for the exact key value. This has zero false positives — it checks for the specific string, not guessed patterns.
Use grep -F (fixed string) and awk for replacement — NOT bare grep/sed — because API keys often contain regex metacharacters (+, /, ., =) that would cause grep to match wrong text and sed to corrupt files.
# Guard: skip if key is empty or too short (< 16 chars). Short strings
# would over-redact legitimate content. Real API keys are 20+ chars.
if [ -n "$API_KEY_VALUE" ] && [ ${#API_KEY_VALUE} -ge 16 ]; then
LEAK_FOUND=false
for dir in "$RESEARCH_DIR" "$PROOFS_DIR" "$DISCOVERY_DIR"; do
if [ -d "$dir" ] && grep -rF "$API_KEY_VALUE" "$dir" 2>/dev/null; then
LEAK_FOUND=true
fi
done
if [ "$LEAK_FOUND" = true ]; then
echo "BLOCKING: API key value found in manuscript artifacts. Auto-redacting."
REDACT_TO="\$${API_KEY_ENV_VAR:-API_KEY}"
for dir in "$RESEARCH_DIR" "$PROOFS_DIR" "$DISCOVERY_DIR"; do
[ -d "$dir" ] || continue
find "$dir" -type f -print0 | while IFS= read -r -d '' f; do
if grep -qF "$API_KEY_VALUE" "$f" 2>/dev/null; then
# Use python for truly literal replacement — awk's gsub and perl's
# s/// both interpret regex metacharacters (+, ., /) in the key,
# which breaks on JWT tokens and base64-encoded secrets.
REDACT_OLD="$API_KEY_VALUE" REDACT_NEW="$REDACT_TO" python3 -c "
import sys, os
old, new, path = os.environ['REDACT_OLD'], os.environ['REDACT_NEW'], sys.argv[1]
with open(path) as f: content = f.read()
with open(path, 'w') as f: f.write(content.replace(old, new))
" "$f"
fi
done
done
echo "Auto-redacted. Verify before proceeding."
fi
fiStrip auth from HAR captures before archiving
Credentials can appear in four locations within HAR files:
- Request headers:
Authorization: Bearer <token>,Cookie: session=... - Response headers:
Set-Cookie: session=<token>(from auth flows) - Query strings:
?key=<value>,?api_key=<value>,?access_token=<value> - Cookies: session tokens, auth cookies (both request and response)
The archive step must strip all four, plus response bodies (for size):
jq 'del(.log.entries[].response.content.text) |
# Remove auth headers from requests
(.log.entries[].request.headers) |= [.[] |
select(.name | test("^(Authorization|Cookie|Set-Cookie|X-API-Key|X-Auth-Token)$"; "i") | not)
] |
# Remove Set-Cookie from responses (contains session tokens from auth flows)
(.log.entries[].response.headers) |= [.[] |
select(.name | test("^(Set-Cookie)$"; "i") | not)
] |
# Redact auth-like query string params
(.log.entries[].request.queryString) |= [.[] |
if (.name | test("^(key|api_key|apikey|token|secret|access_token|password)$"; "i"))
then .value = "<REDACTED>"
else . end
] |
# Remove cookies entirely (they often contain session tokens)
(.log.entries[].request.cookies) |= [] |
(.log.entries[].response.cookies) |= []
' "$har" > "${har}.stripped" 2>/dev/null && mv "${har}.stripped" "$har"API key handling during the run
When the user provides an API key (Phase 0 API Key Gate or inline):
- Store it only in a shell variable, never in a file
- Pass it to commands via environment variable, not via flags visible in process lists
- In dry-run output, the key may appear in query params — this is expected for
debugging but must NOT be captured in proof artifacts
- When writing live smoke results to proofs, write the test outcomes (PASS/FAIL)
but never the request URLs that contain the key in query params
Workspace & organization PII redaction
Live dogfood testing (Phase 5) naturally surfaces workspace-specific data: organization names, team names, team member names, and email addresses. This data must NEVER appear in any public artifact: acceptance reports, shipcheck proofs, manuscripts archived to the library repo, PR descriptions, or READMEs.
When reporting live test results, use generic descriptions:
- "the workspace" not the actual org name
- "5 overloaded members" not their names or emails
- "12 users synced" not "synced matt@company.com, patrick@company.com"
- "team ESP" is OK (team keys are structural, not PII) but "Esper Labs" is not
Before archiving manuscripts (Phase 5.6): Scan acceptance reports and shipcheck proofs for organization names, email addresses, and full names. The exact-value scan for API keys (above) catches secrets; this step catches PII that the user's live workspace naturally produces.
When persisting live-check samples: scorecard --live-check and live-dogfood sample captures scrub output_sample text before it is stored in JSON/proof artifacts. Treat archive-time and publish-time scans as defense in depth, not as permission to write raw customer names, emails, addresses, invoice numbers, or card tails into the run directory.
Before creating publish PRs: The publish skill constructs PR descriptions from manuscripts and test results. Any live test data quoted in the PR body must be scrubbed of workspace PII. The library repo is public.
PII pattern scanning
The prose guidance above tells the agent how to compose published artifacts safely. This section defines a mechanical sweep that runs before publishing to catch the PII the agent missed. Two prior PII leaks happened despite the prose guidance — a mechanical defense layer is required.
Run order: exact-value scan (above) → HAR auth strip → PII pattern scanning.
File scope. Sweep all text-readable files in the staging directory. Detect by content, not extension — a .yaml HAR variant or a .txt proof matters as much as a .md. Use this Go-equivalent helper to classify:
# Treat as text if no NULL byte in the first 8 KiB
isText() { head -c 8192 "$1" | grep -L -q $'\x00' "$1" 2>/dev/null; }(In Bash, file --mime-type works on macOS and GNU file portably enough.)
Tier 1: vendor-anchored auto-redact
Auto-redact ONLY when the pattern includes a vendor-specific prefix anchor. These have near-zero false-positive rate. Auto-redact silently — no user prompt, no recovery prompt. Pattern set:
PII_AUTO_REDACT=(
'openrouter-api-key|sk-or-v1-[A-Za-z0-9_-]{24,}|<REDACTED:openrouter-api-key>'
'bearer-stripe-live|Bearer sk_live_[A-Za-z0-9]{20,}|Bearer <REDACTED:stripe-live-token>'
'bearer-stripe-test|Bearer sk_test_[A-Za-z0-9]{20,}|Bearer <REDACTED:stripe-test-token>'
'bearer-cal-live|Bearer cal_live_[A-Za-z0-9]{20,}|Bearer <REDACTED:cal-live-token>'
'bearer-cal-test|Bearer cal_test_[A-Za-z0-9]{20,}|Bearer <REDACTED:cal-test-token>'
'bearer-github-pat|Bearer ghp_[A-Za-z0-9]{36,}|Bearer <REDACTED:github-pat>'
'bearer-github-oauth|Bearer gho_[A-Za-z0-9]{36,}|Bearer <REDACTED:github-oauth>'
'bearer-github-server|Bearer ghs_[A-Za-z0-9]{36,}|Bearer <REDACTED:github-server-token>'
'bearer-github-fine|Bearer github_pat_[A-Za-z0-9_]{60,}|Bearer <REDACTED:github-fine-grained-pat>'
'slack-user-token|xoxp-[A-Za-z0-9-]{40,}|<REDACTED:slack-user-token>'
'slack-bot-token|xoxb-[A-Za-z0-9-]{40,}|<REDACTED:slack-bot-token>'
'slack-app-token|xapp-[A-Za-z0-9-]{32,}|<REDACTED:slack-app-token>'
'google-api-key|AIza[A-Za-z0-9_-]{20,}|<REDACTED:google-api-key>'
)
for entry in "${PII_AUTO_REDACT[@]}"; do
IFS='|' read -r name regex tag <<< "$entry"
for f in $(find "$STAGING_DIR" -type f); do
isText "$f" || continue
if grep -qE "$regex" "$f" 2>/dev/null; then
perl -i -pe "s|$regex|$tag|g" "$f"
echo "Auto-redacted $name in ${f#$STAGING_DIR/}"
fi
done
doneDo not add a simple AKIA[0-9A-Z]{16} shell auto-redaction rule here. AWS access keys have the same shape as AWS's canonical documentation placeholder, so the binary publish scan handles them with a placeholder allowlist instead.
These patterns are vendor-anchored and the prefix character class is restrictive enough that the false-positive rate is effectively zero. Adding a new vendor pattern is safe — extend the list when shipping a new printed CLI for a vendor with a known token format.
Tier 2: warn-by-default with allowlist
Everything else gets WARN behavior. The user sees the matches, decides whether to redact each one. This includes generic emails, generic bearer tokens (without a vendor prefix), and capitalized name patterns. Designed cost asymmetry: false-positive auto-redaction loses information irreversibly; false-positive warning costs only a prompt.
PII_WARN=(
'email|[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|email address'
'bearer-generic|Bearer [A-Za-z0-9._\-+/=]{20,}|generic bearer token (no vendor prefix)'
'name-pattern|\b[A-Z][a-z]{2,}\s+[A-Z][a-z]{2,}\b|capitalized first+last name'
)Allowlist (suppress matches against this set):
Build at scan time from three sources:
1. Spec-derived terms. Capitalized two-word phrases extracted from the spec's operation summaries, descriptions, tag names, and parameter descriptions. This catches "Event Types", "Booking Links", "Webhook Triggers" automatically — they look like name patterns but are API vocabulary.
2. CLI-help-derived terms. Walk <cli> --help recursively and extract capitalized two-word phrases from command Short/Long fields. Catches command group names that the spec might not mention by exact phrasing.
3. Universal allowlist. A small static list of terms that appear across APIs and would otherwise produce noise:
"Open Source", "Pull Request", "Bearer Token", "API Key", "Access Token",
"Refresh Token", "GitHub Actions", "Cloud Run", "Service Account",
"Rate Limit", "Time Zone", "Web Hook", "Personal Access", "Application Token"A match is suppressed if it matches any allowlist entry by:
- Exact case-insensitive equality, OR
- Case-insensitive
containsagainst the allowlist entry
Email pattern allowlist: any address whose domain matches example.com, example.org, example.net, placeholder.com, or starts with noreply@.
Tier 2 user interaction
When Tier 2 finds matches, present them in a single batched prompt (NOT one per finding — that creates rubber-stamp fatigue). Per-finding format:
PII review (3 findings):
proofs/acceptance.md:42 email trevin@trevinchow.com
proofs/acceptance.md:48 email henryopenclaw@gmail.com
proofs/dogfood.md:108 name Henry Claw
Action:
[a] Auto-redact all
[s] Skip all (proceed without redacting)
[q] Quit and review staging dir at /tmp/.../staging.pre-pii-scrubPre-scrub copy: before any Tier 1 or Tier 2 mutation runs, copy the staging directory to <staging>.pre-pii-scrub/. This gives the user a recoverable checkpoint if a redaction was wrong.
Why warn-by-default
The first design draft auto-redacted emails (permissive pattern) and warned on names (constrained pattern). Two reviewers independently called this cost- asymmetry inverted: false-positive auto-redaction permanently corrupts a manuscript artifact, while false-positive warning costs only a prompt. The correct policy is the inverse — warn on patterns where any false positive would lose information; auto-redact only on vendor-prefix-anchored patterns where the false-positive rate is structurally near-zero.
Worked examples
Example 1: vendor token in a proof file. Input proof file contains:
Authorization: Bearer cal_live_7d8911769f5c3d63811e36d95d873a16Tier 1 matches bearer-cal-live. Auto-redacts to:
Authorization: Bearer <REDACTED:cal-live-token>No user prompt.
Example 2: real attendee in a dogfood report. Input proof file contains:
Returned 1 real booking (Henry Claw 15-min meeting, attendee henryopenclaw@gmail.com).Tier 2 matches email and name-pattern. The email's domain (gmail.com) is not in the allowlist; "Henry Claw" is not in spec-derived terms. Both surface in the batched prompt. User picks [a] Auto-redact all → both redacted.
Example 3: API vocabulary that looks like a name. Input README contains:
The `Event Types` resource lists every bookable meeting type.Tier 2 name-pattern matches "Event Types". Spec-derived allowlist includes "Event Types" (extracted from tag: "Event Types" in the spec). Match is suppressed. No prompt.
Example 4: clean staged dir. No matches in either tier. No prompt, publish proceeds.
Session state cleanup
Session state files (session-state.json) contain live browser cookies and auth tokens captured during an authenticated browser-sniff run. The containment model is by location, not by archive-time cleanup: SESSION_STATE_FILE (set in SKILL.md's "Run Initialization") points at ${TMPDIR:-/tmp}/printing-press-$(id -u)/session/$RUN_ID/session-state.json, outside $DISCOVERY_DIR. The Phase 5.5 archive cp -r "$DISCOVERY_DIR" therefore cannot pick it up, regardless of operator action. After the archive completes, the Phase 5.5 block also wipes $SESSION_DIR so back-to-back runs do not accumulate session state.
A no-op rm -f "$DISCOVERY_DIR/session-state.json" remains in the archive block as a safety net for in-flight runs carried over from a pre-isolation skill version; it is not load-bearing for new runs.
WebSocket-Primary Sync Pattern
Use this pattern when the target API's primary facts arrive over WebSocket and a REST endpoint supplies descriptive metadata needed for local queries.
Spec Shape
streaming:
transport: websocket
url: "wss://api.example.com/v1/ws"
subscribe_shape: '{"type":"subscribe","channels":["events"]}'
framing: newline_delimited_json
metadata:
endpoint: "/v1/events"
refresh_cadence: 30s
statuses: [live, pending]
primary_key: event_idframing may be single_object_per_frame or newline_delimited_json. When metadata.statuses is omitted, the generator polls both live and pending.
Emitted Surface
The generator emits:
live ws syncfor WebSocket-primary ingest.live rest syncas the REST polling fallback, reusing the generatedsynccommand.internal/wsclientwith dial, subscribe, frame splitting, reconnect-with-backoff, and lifecycle event callbacks.- Local SQLite tables
<api>_stream_frames,<api>_stream_metadata, and<api>_rebase_log.
The WebSocket scaffold stores raw JSON facts and raw metadata rows. Per-API novel code may add typed normalizers beside the generated files, but it should not reimplement the WebSocket lifecycle, metadata-status polling, or rebase-log mechanics.
Authoring Rules
- Poll every configured metadata status. Default to both
liveandpending; polling onlylivemisses rows that become live after the frame arrives. - Use
newline_delimited_jsonwhenever a vendor can send multiple JSON objects in one WebSocket message. The generated splitter feeds each line separately tojson.Unmarshal. - Record reconnect, disconnect, subscribe, metadata, and parse events in
<api>_rebase_logso operators can diagnose stream gaps. - Coordinate store writes through the generated live command's shared mutex. Per-API handlers that write additional dimension or fact tables should reuse that pattern.
- Frame normalizers must use
cliutil.ExtractNumberandcliutil.ExtractIntfor numeric wire fields that may be JSON strings.
Related skills
How it compares
Choose printing-press over manual CLI scaffolding when the input is an existing OpenAPI spec, Postman collection, or HAR capture and the deliverable is a ship-ready command-line client.
FAQ
Who is printing-press for?
Developers using agents to execute printing press workflows from SKILL.md.
When should I use printing-press?
When printing-press tasks match the skill activation triggers.
Is printing-press safe to install?
Review the Security Audits panel on this page before installing in production.