Commit Graph

27 Commits

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

* feat(cli): add BLE evidence analyzer

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

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

* feat(cli): add BLE replay adapter

* feat(cli): add BLE replay probe

* feat(cli): add live BLE probe adapter

* feat(cli): classify BLE command guidance

* feat(cli): add BLE session mode tiers

* feat(cli): generate minimal BLE device CLI

* feat(cli): generate replay BLE controls

* feat(cli): generate BLE session scaffold

* feat(cli): generate BLE telemetry store

* feat(cli): show BLE device capabilities

* feat(cli): verify BLE device commands safely

* feat(cli): package live BLE probe testing

* feat(cli): integrate BLE device sniff docs

* refactor(cli): simplify BLE device sniff code

* fix(cli): harden BLE probe evidence workflow

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix(cli): address BLE review feedback

- keep legacy persistent session parsing private

- skip malformed BLE correlation timestamps with report ambiguities

- reuse compiled redaction regexes across evidence fields

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

* feat(cli): add generated BLE session runtime

* fix(cli): skip malformed BLE hex evidence

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

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

* feat(cli): store generated BLE session summaries

* fix(cli): bump Go floor for govulncheck

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

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

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

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

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

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

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

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

* docs(skills): require BLE mapping research gate

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix(cli): address BLE review feedback

* test(cli): refresh BLE skill install goldens

* fix(cli): quote device display names in Go templates
2026-06-05 03:46:49 +00:00
Michael Schreiber ad282805a9 fix(cli): guard empty PROXY_ARGS expansion under bash 3.2 (#2439)
`scripts/install.sh` runs under `set -euo pipefail`. On bash 3.2
(macOS's default `/bin/bash`), expanding an empty array with
`"${PROXY_ARGS[@]}"` triggers `unbound variable` and aborts the
installer at the network probe. Bash 4.4+ fixed this, but the
shebang `#!/usr/bin/env bash` resolves to /bin/bash on stock macOS,
so the documented `curl ... | bash` install was broken there.

Switch both probe sites to the bash-3.2-safe form
`${PROXY_ARGS[@]+"${PROXY_ARGS[@]}"}`, which expands to nothing when
the array is empty and preserves quoting when a proxy is set.

Closes #2438
2026-05-28 02:49:33 +00:00
Vinny Pasceri 72137f9db8 fix(cli): strip =value suffix from --flag=value tokens in verify-skill recipe parser (#2256)
The verify-skill recipe parser recorded `--since=24h` / `--score-gte=7`
tokens verbatim, keeping the `=value` suffix as part of the flag name. The
declared-flag check then compared `--since=24h` against the CLI's `--since`
and reported a false "undeclared flag" positive, flooding clean CLIs with
spurious findings.

Split `--flag=value` on the first `=` so the parser stores the flag name
(`--since`) for the declared-flag comparison. Mirrored byte-for-byte across
the bundled copy and the script source (TestVerifySkillBundledMatchesScript).

Closes #1316

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 04:47:07 +00:00
Trevin Chow fc04a3e6f2 fix(ci): add generated output compile gate (#2319)
* ci(ci): add generated output compile gate

* fix(ci): address generated output review feedback
2026-05-26 03:54:30 +00:00
Vinny Pasceri bf6a610aa5 fix(cli): recognize slice/map/typed pflag variants in verify-skill flag scanner (#2222)
verify-skill's static flag-declaration scanner (FLAG_DECL_RE in
verify_skill_bundled.py + the scripts/ source copy) recognized only a subset
of pflag *Var variants, so commands using IntSliceVar, Float64SliceVar,
BoolSliceVar, IPVar, StringToStringVar, etc. produced false-positive "flag
not declared anywhere" / "declared elsewhere but not on <cmd>" errors —
forcing authors to rewrite typed flags as StringSliceVar + in-handler
parsing. Add the missing same-shape variants to the alternation: the
slice/numeric set (IntSliceVar, Int32/Int64/Float32/Float64/Bool/Duration/
UintSliceVar, Int32Var, Uint32Var, Float32Var) plus net.IP (IPVar, IPSliceVar)
and the map-typed flags (StringToStringVar, StringToIntVar, StringToInt64Var).

Closes #1255

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 21:10:55 +00:00
Matt Van Horn 23b6239c36 feat(cli): generator-wide self-learning CLI loop (#2085)
* docs(cli): plan generator-wide self-learning CLI loop + library sweep

Plan for lifting the prediction-goat self-learning loop into the
generator and sweeping the public library. Includes 11 generator-side
units (Phase 1), pilot sweep into 5 high-value CLIs (Phase 2), and
full library sweep across remaining 163 (Phase 3). Carries quantitative
phase-2 stop thresholds and a rollback playbook for 4 failure modes.

* feat(cli): reserve internal/learn namespace for generator-owned package

The generator-wide self-learning CLI loop (plan
2026-05-23-002) emits an internal/learn/ package into every
printed CLI. Add "learn" to reservedInternalPackages so the
dogfood reimplementation check and source-client check treat
the namespace as generator-owned, mirroring the existing
cliutil/store/mcp/etc. carve-outs. A novel-feature handler
that routes its query through learn.Recall (which delegates
to internal/store for the SQL work) rides the existing store
carve-out automatically.

Updates AGENTS.md "Generator-reserved namespaces" to list
internal/learn/ alongside internal/cliutil/ and
internal/mcp/cobratree/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): add LearnConfig spec block for self-learning CLI loop

Introduce a `learn:` block on APISpec that carries the per-CLI shape
the generator will use to wire the self-learning loop into every
printed CLI:

* TickerPatterns -- Go regexp patterns the recall path uses to
  recognize resource identifiers in free-text queries. Validated
  via regexp.Compile at spec load so authoring typos fail fast.

* Stopwords -- domain-specific tokens stripped from queries before
  matching against learned templates. Whitespace-only entries are
  dropped at parse time, mirroring the runtime entities.Config
  behavior the generated CLI will apply.

* EntityLookupSeeds -- canonical-name + aliases table keyed by seed
  kind (e.g., "country", "team"). Drives the entity substitution
  that lets one teach generalize across an entity's siblings.

Validation rejects invalid regexps (named index + regex error),
seed kinds containing whitespace or punctuation other than `_`,
empty Canonical values, and duplicate canonical values within the
same kind. Absent or empty `learn:` blocks parse to a zero-value
LearnConfig and never run any validation, so existing specs are
unaffected.

This is U1 of the generator-wide self-learning CLI loop plan
(docs/plans/2026-05-23-002). Subsequent units wire the loop into
the template emission, dogfood reimplementation check, and the
library retrofit sweep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): bump store schema to v3 with additive learn tables

Stamps StoreSchemaVersion = 3 unconditionally across every printed CLI and
emits additive CREATE TABLE IF NOT EXISTS migrations for the self-learning
loop's local-store backing (search_learnings, search_patterns, entity_lookups,
teach_log_metadata, search_learnings_fts) when Spec.Learn.Enabled. The version
stamp is uniform across learn-enabled and learn-disabled prints so library CLIs
move forward together; the gate hides the table creates so the on-disk surface
of learn-disabled CLIs is unchanged. The migrations slice carries an anchor
comment ("CLI Printing Press: learn migrations") for the published-library
sweep tool to locate during retrofit.

Adds a minimal LearnConfig stub (Enabled bool only) to APISpec so the template
gate compiles ahead of U1's full struct + validation work. The legacy v2 -> v3
upgrade path is covered by a new TestMigrate_V2ToV3IsAdditive in the schema-
version test template that seeds a v2-stamped DB, reopens with the new binary,
and asserts the pre-existing resources row survives the migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): remove duplicate Learn field after U6 merge

The U6 merge resolution kept U1's LearnConfig struct but the field
declaration on APISpec landed twice (U1 inserted near line 195, U6
inserted near line 198). Dedup so the package compiles.

* feat(cli): add scorecard credit and CI purity gate for learn package

Wires the surrounding pipeline up to the generator-wide self-learning loop
emitted by U3-U5 of plan 2026-05-23-002:

- scoreVision: +0.5 tier-1 credit when internal/learn/learn.go is present.
  Modest by design — value compounds with teach traffic at runtime, not at
  print time.
- regenmerge: fixture confirming new internal/learn/*.go emission classifies
  as VerdictNewTemplateEmission (clean overwrite) when the published CLI
  predates the loop.
- reimplementation_check: fixture confirming a handler that mixes
  learn.Recall+learn.Teach with a real client call passes the dogfood
  signal — the realistic agent-authored shape once the loop ships.
- scripts/verify-learn-purity.sh + .github/workflows/verify-learn-purity.yml:
  CI gate that fails if learn-loop templates leak domain identifiers
  (kalshi, polymarket, country_iso2, prediction_goat). Strips // comments
  before grepping so lineage notes in doc.go.tmpl stay legal. Passes when
  no learn templates exist yet so the gate ships ahead of U3-U5.

* feat(cli): add learn entities + lookups subpackage templates

Land the two leaf subpackages of the self-learning loop port: a
domain-neutral entity extractor (entities package) and a
canonical-to-alias lookup table backed by the v3 entity_lookups
schema (lookups package). Both are config-per-call: every public
function takes its dependencies explicitly so there is no package-
level singleton state, which lets multiple Config / DB handles
coexist in a single CLI process (important for tests and for any
future multi-tenant CLI surface).

The entities package ships a Config builder, RegisterTickerPattern,
RegisterStopwords, and an Extract(query, *Config) Result function
that classifies tokens into Entities / Tickers / NonEntityTokens.
Default stopwords are domain-agnostic English filler; CLI-shape
stopwords (ticker words, generic question vocabulary) are
registered per-CLI via the spec's Learn.Stopwords. No baked-in
ticker patterns; each consumer CLI registers its own via the spec.

The lookups package ships table-backed Upsert / Lookup / LookupAll
plus the computed-kind transforms (lowercase, uppercase, kebab-case,
capitalize-first, slug). Computed kinds short-circuit the DB and
return a transformed value without a row lookup; the closed set
keeps recipe portability across CLIs that have not seeded any
domain reference data. SeedFromConfig consumes the spec's
LearnConfig.EntityLookupSeeds map and writes canonical-+-alias
rows into entity_lookups under the source='seeded' tag.

Domain neutrality: per the U11 CI purity gate, no kalshi / polymarket
/ country_iso2 / prediction_goat identifiers appear in any template
source. Test fixtures use neutral names (Alpha / Bravo / Charlie /
TICKER-123 / slug-thing-foo-456).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): add learn patterns subpackage with generalization engine

Land the patterns subpackage: the generalization layer of the
self-learning loop. When a user has taught two or more structurally-
similar queries to two or more structurally-similar resources, the
Extract pass groups them by structural signature, finds the lookup
kind whose values reproduce every member's resource ID under one
shared template, and writes the result to search_patterns. On a
future recall whose entity was never directly taught, Apply
substitutes via lookups.Lookup, verifies the candidate exists in
the resources table (or matches a prefix LIKE search for the
substitute-then-search-prefix strategy), and emits a Hit with
source="pattern".

Schema adaptation: the v3 search_patterns row is
(template TEXT, entity_kind TEXT, confidence INTEGER, source TEXT,
 created_at) with PK (template, entity_kind). The Pattern struct
carries a richer shape (query_template + resource_template +
resource_type + venue + strategy + examples); we encode that bundle
as a JSON envelope in the `template` column so the package surface
stays expressive without changing the schema.

Lineage: the recipes package shipped in prediction-goat-pp-cli is
renamed to patterns / Pattern / search_patterns across the generator
template so the same code can ship into CLIs whose domain
vocabulary already uses "recipes" for its own purpose (food,
cooking, etc.). The substitution-strategy taxonomy and binding
search algorithm are unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): lift self-learning loop into generator templates

Land the learn package core (normalize / match / recall / teach /
teach_log / preseed) plus the generator wiring that emits the
internal/learn/ tree into every Learn.Enabled CLI. Completes the
generator-side port of the prediction-goat smart-learning subsystem;
combined with the U1 spec block, U2 namespace reservation, U6 v3
store schema, and U11 CI purity gate already on the feature branch,
the loop is now fully generator-owned.

Architecture: every public function takes a *entities.Config (built
once at CLI startup via NewConfig + the spec's Learn.TickerPatterns
and Learn.Stopwords) and an explicit *sql.DB. No package-level
singleton state -- this is the "config-per-call" refactor called
out in the plan's Key Technical Decisions section. The store
package never imports learn; the v3 schema is already pre-created
at the right shape by store.go.tmpl's gated migrations slice, so
no v3->v4 data backfill is needed and none is performed here
(option a in the migration question: the learn package owns its
own data lifecycle).

Generator wiring: prepareOutput reserves internal/learn{,/entities,
/lookups,/patterns} when Spec.Learn.Enabled, and the new
renderLearnFiles() method emits the 24 template files under that
same gate from renderOptionalSupportFiles, following the
renderShareFiles precedent at line 1727.

Schema adaptation: search_learnings in v3 is keyed by
(query_pattern, resource_type) with a JSON resource_ids array per
row (not the per-(query, resource) row shape the prediction-goat
schema had). Teach appends a new resource id to the array and
bumps confidence; Recall expands the array on read and emits one
Hit per id. ResourceEntitiesFromJSON is the generic
resource-shape-extractor the recall path uses; consumer CLIs pass
the per-resource-type field map via Opts.ResourceTypeFields rather
than hard-coding the dispatch in this generator template.

Domain neutrality: the U11 CI gate verifies no kalshi / polymarket
/ country_iso2 / prediction_goat strings appear in any learn
template body. Lineage notes (with a prediction-goat reference)
live in learn/doc.go.tmpl and learn_patterns/doc.go.tmpl, both of
which the gate allowlists via comment stripping.

Tests: 1140 unit tests pass in internal/generator; the new
TestGenerateLearnPackageCompilesAndTests drives the emitted learn
package through `go test ./internal/learn/...` against a fixture
spec, exercising the full template -> emit -> compile -> test pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): emit teach/recall/learnings commands into learn-enabled CLIs

U7 of the generator-wide self-learning loop. Lifts the cobra surface
(teach, recall, learnings, learnings forget, teach-pattern,
teach-lookup) from prediction-goat into a domain-neutral
internal/generator/templates/teach.go.tmpl, gated on Learn.Enabled.

Templates added:
- teach.go.tmpl: ~700 lines of carved-out cobra commands. Each command
  takes the per-CLI *entities.Config built by newLearnConfig() (U7
  placeholder; U8 will replace with a spec.Learn-driven shim). recall
  and learnings list carry mcp:read-only=true; the write commands are
  unannotated. --resource-type is required + explicit on teach.
- teach_test.go.tmpl: ported tests for the lifted surface plus
  skip-list contract assertions.

root.go.tmpl modifications:
- Adds a noLearn rootFlag + --no-learn persistent flag, gated on
  Learn.Enabled.
- Registers the six commands conditionally and constructs the per-CLI
  learnCfg once.
- Introduces learnHookSkipList + shouldSkipLearnHook(): forward-looking
  framework for a future auto-recall PersistentPreRunE hook. The skip
  list (auth, doctor, help, sync, profile, feedback, which,
  agent-context, completion, version) has no runtime consumer in v1
  because teach/recall are agent-driven explicit calls; the machinery
  sits in place so a later integration doesn't re-derive the set.

Spec reservation:
- Adds learnings, recall, teach, teach-lookup, teach-pattern to
  spec.ReservedCobraUseNames so a future API spec resource named
  "teach" can't shadow the framework command at parse time.

Tests:
- TestGenerateLearnCLICommandsCompileAndTest exercises the printed
  CLI end-to-end: builds it under -mod=mod and runs the lifted test
  surface against the emitted code.
- TestGenerateLearnPackageEmitsAllFiles + TestGenerateLearnPackageGatedOff
  extended to assert internal/cli/teach.go presence/absence under
  Learn.Enabled.

Golden suite: passes (root.go.tmpl skip-list block is fully gated on
Learn.Enabled, agents.md.tmpl section uses left-trim markers so the
gated-off output is byte-identical to the baseline).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): document self-learning loop in SKILL.md + README.md templates

Adds a learn-enabled "Automatic learning" section to skill.md.tmpl and a
"Self-learning loop" subsection to readme.md.tmpl, both gated on
.Learn.Enabled. The skill section mirrors the four-branch protocol
generalized from prediction-goat (recall hit / pattern apply / cold start /
verification failure) in CLI-neutral language; per-CLI domain examples
come from the spec's learn: block at print time.

Also expands AGENTS.md:
- Adds a note under Generator-reserved namespaces pointing at the
  verify-learn-purity.yml CI gate and the domain-neutrality requirement
  for internal/generator/templates/learn**.
- Extends the Cross-repo dependency section to cover learn-loop template
  shape changes alongside the existing readme.md.tmpl / skill.md.tmpl
  contract, so the library-side sweep tool stays in lockstep when the
  internal/learn package shape shifts.

Agent-context output requires no template change: the runtime walker in
agent_context.go.tmpl auto-discovers root cobra commands and their MCP
annotations, so the teach/recall/learnings/teach-pattern/teach-lookup
commands registered under .Learn.Enabled in root.go appear in the JSON
surface automatically.

Golden suite passes unchanged (no fixture has learn: enabled yet, so the
conditional sections don't fire). Per-fixture diffs will land in U12.

* feat(cli): wire spec.Learn into runtime config + seed init

Replace U7's placeholder newLearnConfig() with the real spec-driven
init shim: learn_init.go.tmpl emits newLearnConfig() that translates
spec.Learn.TickerPatterns + Stopwords into a configured *entities.
Config, and initLearn() that materializes spec.Learn.EntityLookupSeeds
into the entity_lookups table via lookups.SeedFromConfig.

A sync.Once-guarded runLearnInitOnce wires into root.go's
PersistentPreRunE so the seed pass runs once per process for any
non-skiplist command. Soft validation throughout: runtime regex
compile failures log to stderr and skip the pattern; store-open or
seed failures degrade to stderr warnings instead of aborting the CLI.

Tests cover the spec-driven literal emission, the empty-config
import gate (no unused regexp / lookups imports when the spec
declares neither), idempotent seeding, and nil-db safety.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(cli): add generate-learn-loop-api golden fixture

Locks the spec-declared self-learning loop emission contract end-to-end.
The fixture mirrors generate-sync-walker-api's multi-file shape and
adds a full learn: block with neutral identifiers (EXAMPLE-* ticker,
ALPHA/BRAVO entities) so scripts/verify-learn-purity.sh stays green.

The artifacts manifest locks the spec-derived emission contract:
- provenance manifests (.printing-press*.json, manifest.json)
- internal/cli/learn_init.go (spec-derived newLearnConfig + initLearn)
- internal/cli/teach.go (the 6 teach/recall/learnings commands)
- internal/cli/root.go (--no-learn flag + learn-conditional registrations
  + learnHookSkipList)
- internal/store/store.go (StoreSchemaVersion = 3 + additive CREATEs
  for search_patterns, entity_lookups, teach_log_metadata)
- one representative file per learn subpackage so a rename/signature
  drift trips the gate
- SKILL.md, README.md, AGENTS.md self-learning sections

Without the learn-block emission path every artifact would regress to
the non-learn shape.

* fix(cli): drop obsolete loop-var capture in learn_store_test

Go 1.22+ no longer needs the tc := tc shadow; modernize linter flagged
the loop in TestU6 schema-version test.

* Update scorecard.go

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update verify-learn-purity.sh

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* docs(cli): add Phase 1 + Phase 2 execution retrospective to plan

Captures the 4-iteration pilot cycle, the 4 sweep-tool bugs surfaced (A:
schema-const splice, B: ptr-vs-value flags, C: emitted helper deps, D:
missing closing parens in SQL), the fresh-HOME validation lesson, and
the validation-gate ordering that catches each bug class.

Documents for future Phase 3 work: tool is sound, but expect class-of-
bugs that only surface against real library artifacts.

* fix(generator): gate StoreSchemaVersion on Learn.Enabled

Non-learn CLIs stay at v2 with no schema delta; learn-enabled CLIs
advance to v3 for the learn tables. The v2->v3 migration test is
wrapped in the same {{if .Learn.Enabled}} gate since there is no
migration to assert on a non-learn CLI.

Goldens refreshed: sync-walker (non-learn) drops from 3 back to 2;
learn-loop-example renders the gate to 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(generator): assert non-learn store stays at v2

Replaces the prior test that locked in the uniform v3 bump with two
invariants: disabled spec emits v2 and zero learn-table DDL; enabled
spec advances to v3 and emits all five learn-table creates.

Also adds TestGenerateLearnEnabledRequiresStoreVision covering the
companion validation gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(generator): reject learn.enabled without store vision

learn.enabled requires internal/store to be emitted; the learn package
imports it. Without the gate, a spec with learn.enabled: true and no
store vision produced uncompilable code. Now prepareOutput() errors
out with a clear message after constrainVisionTemplates finalizes the
vision set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(generator): ancestor-aware learn-hook skip via CommandPath

cmd.Name() returns the leaf's first Use-field word, so "auth login"
slipped past the "auth" skip entry and triggered runLearnInitOnce
on framework commands. Now shouldSkipLearnHook iterates strings.Fields
of cmd.CommandPath() and matches any segment against learnHookSkipList.

Added TestSkipLearnHook_FrameworkCommandsDoNotCreateDefaultDB asserting
the observable side-effect (no default DB created) for auth setup,
completion bash, version, help, doctor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(generator): domain-neutral teach-pattern example and extend purity gate

The teach-pattern Long: text and Example: block shipped into every
learn-enabled CLI carrying prediction/sports-market wording ("odds
{entity} wins world cup", country entity-kind). Neutralized to
"items in {entity}" / category entity-kind.

verify-learn-purity.sh now scans teach*.go.tmpl and learn/*.go.tmpl
in addition to learn*.go.tmpl and learn_*/*.go.tmpl, so domain-word
leakage into the emitted learn CLI surface is caught at CI time (32
templates scanned, was 17).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pipeline): restore import.go credit and drop dead learn.go check in scorecard

The prior web-edit pointed the learn presence check at internal/learn/
doc.go (the file the generator actually emits) but accidentally removed
the unrelated import.go presence credit and left the dead learn.go
check in place.

Now: import.go credit restored, dead learn.go check removed, comment
cleaned up. New TestScoreVision_LearnLoopGoldenFixtureAddsHalfPoint
asserts the credit fires against the real generate-learn-loop-api
golden fixture; TestScoreVision_ImportPresenceAddsHalfPoint covers
the restored import.go credit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(generator): refresh learn-loop goldens for main-merge

Picks up two upstream main refactors that landed since this branch
diverged: the version command's Printf now uses cmd.Root().Name()
instead of hardcoding the project slug, and upsertLeaguesTx's doc
comment now describes the per-resource domain-table portion.

Both files are emitted artifacts; the source templates themselves
already had these shapes on main.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(cli): authoring guide for spec.Learn with ESPN worked example

Documents how to populate ticker_patterns, stopwords, and entity_lookup_seeds.
ESPN serves as the worked example with full NFL (32) / NBA (30) / MLB (30) /
MLS (30) team rosters and 2-4 aliases per team — the data set a real ESPN
learn block would carry to make recall generalize across leagues.

Also includes the local validation workflow (fresh-HOME smoke testing per
the predecessor plan's retrospective), the library sweep workflow for
already-published CLIs, and common pitfalls.

Companion plan at docs/plans/2026-05-24-001 captures the broader pilot
authoring scope (U1: spec block, U2: regenerate, U3: library sweep, U4:
this guide) for resumption from a fresh window.

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Trevin Chow <trevin@trevinchow.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-05-25 03:18:53 +00:00
Trevin Chow 5acaa2adac fix(cli): ignore shell operators in verify-skill positionals (#2095)
* fix(cli): ignore shell operators in verify-skill positionals

* fix(cli): address verify-skill review findings
2026-05-24 23:51:17 +00:00
Trevin Chow a1b96ff74d feat(installer): add repo bootstrap script (#2044) 2026-05-24 07:00:14 +00:00
Trevin Chow 01afec0970 fix(cli): require quoted shell variables in skill bash blocks (#2004)
* fix(cli): require quoted shell variables in skill bash blocks

* docs(cli): update verify-skill check count
2026-05-23 23:00:38 +00:00
Trevin Chow d328375cf1 fix(cli): distinguish verify-skill prose invocations (#1906)
* fix(cli): distinguish verify-skill prose invocations

* fix(cli): ignore prose separator flags
2026-05-23 18:28:47 +00:00
Trevin Chow 7c4beddbb6 feat(cli): rename generator binary to cli-printing-press (#1722)
* feat(cli): rename generator binary to cli-printing-press

* fix(cli): address legacy command review feedback
2026-05-20 19:57:15 -07:00
Trevin Chow 595ed9ec92 fix(cli): require documented flags in verify-skill (#1707) 2026-05-20 00:21:33 -07:00
JustSpellJ 75e4d1b924 fix(cli): verify-mode HTTP-verb short-circuit + N1 envelope + doctor + read-only POST bypass (#1398)
Closes plan 2026-05-13-001 (U1-U10), the N1 envelope follow-up surfaced in the petstore agent-readiness review, and the greptile P1 read-only-POST finding.

- **Transport-layer gate** (`client.do()`): mutating verbs (DELETE/POST/PUT/PATCH) under `PRINTING_PRESS_VERIFY=1` short-circuit with a synthetic `{"__pp_verify_synthetic__":true,"status":"noop","reason":"verify_short_circuit",...}` envelope. `PRINTING_PRESS_VERIFY_LIVE_HTTP=1` opts back into the real wire for the verify pipeline's mock-mode subprocesses.
- **Read-only POST bypass** (greptile P1): `client.do()` is now a thin wrapper around `doInternal(..., readOnlyIntent bool)`; a new `doRead()` passes `readOnlyIntent=true`. New `PostQueryWithParams` / `PostQueryWithParamsAndHeaders` route through `doRead`. The endpoint template emits `PostQuery*` for `.IsReadOnly` POST commands so GraphQL queries, JSON-RPC reads, and POST-based search dial through the real transport even under verify mode.
- **N1 envelope propagation**: outer command envelope reports `verify_noop: true` and `success: false` when the synthetic sentinel is detected. Naive validators no longer read the noop as a real mutation.
- **Doctor diagnosis line** (U9): `<cli> doctor` surfaces verify state ("normal operation" / "ACTIVE — short-circuit" / "ACTIVE — live HTTP opt-in") so an operator inheriting `PRINTING_PRESS_VERIFY=1` detects the foot-gun without inspecting a response body.
- **Live-verifier env strip** (U10): `live_dogfood` and `workflow_verify` strip both verify env vars from subprocess env via `filterVerifyEnv` so they cannot inherit verify-mode short-circuiting.
- **Per-CLI emitted test** (U5): every regenerated CLI ships `internal/client/client_verify_short_circuit_test.go` covering all four gate states (mutating + verify, LIVE_HTTP opt-in, no env, GET control) plus a `_ReadOnlyPOST` sub-test asserting `doRead` dials through. A template edit that drops the gate fails downstream `go test` rather than silently re-opening the readiness gap.
- **Docs** (U6): AGENTS.md "Side-effect commands" section + `cliutil_verifyenv.go.tmpl` docstring document the transport-layer gate, the LIVE_HTTP opt-in, the `doRead` read-only bypass, the live-verifier strip, and the doctor diagnosis anchor.
- **Regen-merge fixture** (U7): pins `TEMPLATED-WITH-ADDITIONS` classification for the canonical scenario where an operator has hand-added a top-level helper to `client.go` and the fresh template introduces the short-circuit.
- **Rollout playbook** (U8): `docs/solutions/best-practices/verify-mode-short-circuit-rollout-2026-05-13.md` covers the tier:official-first sweep order and known foot-guns.

Verified live on the petstore smoke pipeline: `PRINTING_PRESS_VERIFY=1 petstore-pp-cli pet delete 42 --json` returns the synthetic envelope with no network call.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-05-19 22:12:32 +00:00
Trevin Chow c3e1b87aa9 feat(cli): emit .printing-press-patches.json on every generate (#1590) 2026-05-17 10:30:22 -07:00
Trevin Chow a1098f1563 fix(cli): populate printer + run_id in generated .printing-press.json (#1442)
* fix(cli): populate printer + run_id in generated .printing-press.json

Standalone `printing-press generate` left both fields empty by default, so
publish-validate's manifest contract failed on every freshly generated CLI
that polish hadn't hand-patched.

- resolvePrinterForNew now falls back to `gh api user --jq .login` when
  `git config github.user` is unset, covering machines where the user has
  authenticated gh without setting the git config.
- WriteManifestForGenerate auto-fills an empty RunID with a fresh
  YYYYMMDD-HHMMSS so the manifest always has a non-empty run_id. Phase 5
  callers still need the research-dir-derived ID and root.go keeps its
  --research-dir warning for that case.
- Golden harness normalizes run_id alongside generated_at; the five
  affected fixtures regain a stable shape.

Closes #1348

* fix(cli): harden printer fallback against gh null output and unisolated PATH

Address Greptile P2 review on #1442:

- Reject the literal string "null" from `gh api user --jq .login` (jq emits
  this when the API response lacks `.login` or returns JSON null); without
  the filter the printer field would be set to "null" and still fail
  publish-validate the same way an empty printer does.
- stubBinaryDir now prepends to PATH instead of replacing it entirely so
  TestGenerateLeavesPrinterEmptyWhenGitHandleUnset (which calls the broader
  gen.Generate(), not just resolvePrinterForNew) keeps non-stubbed binaries
  reachable.
- Add TestResolvePrinterForNewRejectsGhNullOutput.
2026-05-15 18:50:20 +00:00
Trevin Chow 73fe56730b fix(cli): extend verify-skill recipe scan to README.md (#1430)
* fix(cli): extend verify-skill recipe scan to README.md

verify-skill previously walked only SKILL.md's bash recipes, so README
Quick Start blocks with broken commands or undeclared flags shipped
undetected. Walk README.md too when present, and tag every finding with
its source file in evidence / detail so users can locate the offending
block.

The four checks (flag-names, flag-commands, positional-args,
unknown-command) all now iterate over the prose sources list. The
SKILL.md-specific `## Command Reference` inline backtick surface stays
gated to SKILL.md because README has no equivalent canonical section.

Closes #1152.

* fix(cli): scope verify-skill flag-names dedup per source

Address Greptile P2 finding on PR #1430. flag-names previously used a
seen set spanning all prose sources, so a flag undeclared in both
SKILL.md and README.md fired only once tagged SKILL.md. If the user
edited SKILL.md but left the same broken example in README.md, the
verifier returned a false "fixed" signal.

Scoping the seen set per source makes flag-names match flag-commands's
per-source emission policy: each source produces its own finding.
2026-05-15 07:43:14 +00:00
Trevin Chow cb1697ed2f fix(cli): force UTF-8 stdio in verify-skill Python subprocess (#985)
Windows consoles default to cp1252, which cannot encode the ✓/✘ glyphs
the verify-skill Python script prints. The subprocess crashed with
UnicodeEncodeError mid-print and the shipcheck umbrella reported FAIL
even when the underlying checks all passed.

Belt-and-suspenders fix:
- Python side: sys.stdout/stderr.reconfigure(encoding="utf-8") at the
  top of main() in scripts/verify-skill/verify_skill.py (and the bundled
  copy at internal/cli/verify_skill_bundled.py). Self-contained, no env
  dependency, covers direct python3 invocations.
- Go side: pythonUTF8Env() helper forces PYTHONIOENCODING=utf-8 and
  PYTHONUTF8=1 on the subprocess env, overriding any conflicting parent
  values. Robust even if a future edit removes the Python-side guard.

Either change alone closes the bug; both together is defense in depth.

Closes #976
Closes #819
2026-05-10 23:47:55 +00:00
Trevin Chow d0086ffe38 fix(cli): handle Windows shipcheck paths (#783) 2026-05-09 16:58:12 +00:00
Trevin Chow c69e77c810 feat(cli): scope verify-skill flag-names; add canonical-sections check (#665)
The trigger-dev / linear SKILL.md transcripts revealed two failure modes
in the polish loop: verify-skill's flag-names check regex-scanned the
entire SKILL.md, so external-tool flags (e.g. --cli-only on the npx
installer line) tripped a false positive that pressured agents into
stripping the flag — and in one case fabricating a /ppl install slash
command — to silence the gate.

Two layered fixes:

  1. flag-names is now scoped to <cli_binary> ... recipes via
     extract_recipes (verify_skill.py). Flags on lines that invoke other
     tools (npx, gh, go install, curl, ...) are out of scope by design.

  2. New canonical-sections check verifies the printed SKILL.md's
     "## Prerequisites: Install the CLI" block matches what the generator
     would emit today — re-rendered from .printing-press.json + go.mod.
     Hand-edits to this generator-owned section now fail verify-skill
     with an explicit "regenerate, don't hand-edit" message, blocking the
     install-section drift class entirely (flag strip, fabricated install
     methods, duplicated fallback blocks).

CanonicalSkillInstallSection lives in internal/generator and stays in
lockstep with skill.md.tmpl via TestCanonicalSkillInstallSectionMatchesTemplate.
The runtime check fires through both the cobra verify-skill command (used
by polish/shipcheck) and publish package validation.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:27:30 -07:00
Trevin Chow fd7fa6eaec feat(cli): Hermes/OpenClaw frontmatter alignment for printed CLIs (#655)
* docs(cli): add Hermes/OpenClaw frontmatter alignment plan

Captures the v1 scope: strip OpenClaw env-var declarations (false-positive
risk on harvested credentials), add Hermes-recognized top-level fields,
move the existing CLI installation section near the top of SKILL.md so
agent hosts read it as a precondition, add Install via Hermes/OpenClaw
README sections.

Cross-repo work tracked: U5 (workflow trigger fix) and U6 (one-time sweep
tool) in printing-press-library; 5 reprints in #654.

Supersedes 2026-05-06-001 after two doc-review rounds.

* feat(cli): plumb pressVersion helper and OwnerName field for Hermes

Adds the two pieces of generator infrastructure that the Hermes
frontmatter emission (forthcoming U1) needs:

  * `pressVersion` template helper with two-tier resolution. When
    .printing-press.json exists at the output dir (regen / sweep
    path), reads `printing_press_version` so the SKILL.md `version:`
    field stays stable across Press releases that don't touch this
    CLI. When the manifest is absent (fresh-print path — the manifest
    is written during publish, after templates render), falls back
    to the live version.Version constant.

  * `OwnerName` field on spec.APISpec, sourced via
    `resolveOwnerNameForExisting` / `resolveOwnerNameForNew`. Reads
    raw `git config user.name` — no sanitization (preserves "Trevin
    Chow" verbatim, which `resolveOwnerForNew`'s sanitizeOwner would
    have collapsed to "trevin-chow"), no fallback to "USER" (would
    publish an obviously-wrong author). Empty values are rejected
    in Generate() before any file write so a broken `author:` field
    never reaches the public library.

OwnerName is distinct from the existing slug-shaped Owner field,
which continues to drive Go module paths and copyright headers.
The two coexist on the spec: slug for paths, prose for prose.

U4 of docs/plans/2026-05-06-002-feat-hermes-openclaw-frontmatter-alignment-plan.md

* feat(cli)!: emit Hermes-recognized top-level fields in SKILL.md, drop OpenClaw env declarations

Strips the OpenClaw env-var declaration shapes from the SKILL.md template:

  * `metadata.openclaw.requires.env` (the conditional env list)
  * `metadata.openclaw.envVars` (the rich envVars block, with all
    its Kind-aware description branching)

The classification problem these blocks tried to solve — distinguishing
user-set credentials (api_key tokens) from harvested ones (cookies, OAuth
refresh tokens) — is asymmetric on failure. False-positive on a harvested
var prompts the user for a value the CLI can't accept. The two tooling
attempts that landed (PR #632/#639's EnvVarSpecs widen, then various
classifier predicates) each surfaced new edge cases at the boundary.
v1 ships no env-var declarations in either format; the existing
auth.Type-branched README content (api_key→env var, cookie/composed→
auth login --chrome, oauth2→auth login, bearer_token→set-token or env
var) carries credential UX without re-introducing the classification
risk.

Adds the Hermes-recognized top-level frontmatter fields:

  * `version: "{{pressVersion}}"`  — two-tier resolver from U4
  * `author: "{{yamlDoubleQuoted .OwnerName}}"`  — prose name
  * `license: "Apache-2.0"`  — constant; printed CLIs are already
    Apache 2.0 via LICENSE.tmpl

The OpenClaw `requires.bins` and `install` blocks stay as-is — they're
the structured install signal OpenClaw consumers depend on. Hermes
ignores unknown keys per its docs; the two formats coexist in one
SKILL.md without a versioned schema gate.

BREAKING CHANGE: SKILL.md frontmatter shape changes for every printed
CLI on next regen. The library-wide sweep that lands the same shape
into already-published library entries is U6 (forthcoming, in
printing-press-library).

U1 of docs/plans/2026-05-06-002-feat-hermes-openclaw-frontmatter-alignment-plan.md

* feat(cli)!: move CLI install instructions to Prerequisites section at top of SKILL.md

Real-world signal from a Hermes session: the agent loaded a printed-CLI
skill but didn't install the underlying binary because the install
information sat at line ~348 of the rendered SKILL.md — after Command
Reference, after Auth Setup, after Argument Parsing. LLM-driven agents
read top-down and decided what to run before reaching it. The install
metadata lived in `metadata.openclaw.requires.bins` + `install`, but
Hermes doesn't read OpenClaw metadata, so the only signal an agent
host's LLM had was buried prose.

Moves the existing `## CLI Installation` section to immediately after
the H1, renames it `## Prerequisites: Install the CLI` to signal a
precondition, and rewrites the lead with imperative wording:

  > This skill drives the `<cli>-pp-cli` binary. **You must verify
  > the CLI is installed before invoking any command from this
  > skill.** If it is missing, install it first: ...

The bold "must verify" + explicit failure mode at the bottom ("Do not
proceed with skill commands until verification succeeds") signals it
as instruction, not reference. Plain markdown — any agent host's LLM
reads it regardless of frontmatter format support.

Updates the Argument Parsing routing rule and the Direct Use lookup
to point at "Prerequisites" instead of the now-removed "CLI
Installation" heading. MCP Server Installation stays in its existing
location (different surface, different audience).

BREAKING CHANGE: SKILL.md body shape changes for every printed CLI
on next regen. The library-wide sweep that lands the same shape into
already-published library entries is U6 (forthcoming).

U2 of docs/plans/2026-05-06-002-feat-hermes-openclaw-frontmatter-alignment-plan.md

* feat(cli): add Install via Hermes and Install via OpenClaw README sections

Slots two new install sections after Use with Claude Desktop, before
the Manual JSON config <details> block. Both formats use the full
mvanhorn/printing-press-library/cli-skills path — earlier drafts in
the conversation used a shorter mvanhorn/cli-skills form for the
Hermes CLI command that doesn't resolve in practice.

Hermes section emits both forms (CLI install + chat-session /skills)
in bash code fences. OpenClaw section emits the user-confirmed
"Tell your OpenClaw agent (copy this)" wording inside a plain code
fence so the operator can paste the agent instruction verbatim.

A `<!-- pp-hermes-install-anchor -->` HTML comment marks the insertion
point so the cross-repo sweep tool (U6, forthcoming) can locate the
position idempotently when it patches legacy READMEs that predate
this template change. Sweep-tool fallback chain (Use with Claude
Desktop → Use with Claude Code → ## Install → EOF) handles READMEs
without the anchor.

URLs hardcode mvanhorn/printing-press-library; no abstraction layer
for what's currently a single constant.

U3 of docs/plans/2026-05-06-002-feat-hermes-openclaw-frontmatter-alignment-plan.md

* chore(cli): retire tools/migrate-skill-metadata

The migrator was a one-shot historical conversion that ran across the
library to convert legacy `metadata: '{...JSON-string...}'` frontmatter
into the nested-YAML shape. Long since landed; the tool has been
dormant.

Its emission shape had drifted from the live skill.md.tmpl template:
still emits `primaryEnv`, requires.bins-before-env ordering, no
envVars block. After U1's changes (strip OpenClaw env declarations,
add Hermes top-level fields), the drift is wider — re-running this
tool would actively regress published SKILL.md files. Delete to
remove the foot-gun.

Verified no internal imports reference the package. Removes the
matching `/migrate-skill-metadata` line from .gitignore (the binary
output that line covered no longer has a source).

If a similar one-shot migration is ever needed in the future, write
a fresh tool then; the precedent (line-targeted text replacement,
idempotency tests) is captured in commit history.

U7 of docs/plans/2026-05-06-002-feat-hermes-openclaw-frontmatter-alignment-plan.md

* test(cli): update golden fixtures for Hermes/OpenClaw frontmatter alignment

Regenerates the SKILL.md and README.md fixtures for both
generate-golden-api and generate-golden-api-rich-auth cases. Diffs
are exactly the intentional changes from U1, U2, U3:

  * Top-level Hermes fields (`version`, `author`, `license`)
    appear after `description:` (U1)
  * `requires.env` line and `envVars:` block removed from the
    OpenClaw block (U1)
  * `## Prerequisites: Install the CLI` section appears after H1,
    before the value-prop paragraph (U2)
  * `## CLI Installation` section removed from its old position
    after Argument Parsing (U2)
  * Argument Parsing rule and Direct Use lookup updated to
    reference Prerequisites (U2)
  * `## Install via Hermes` and `## Install via OpenClaw` sections
    appear after Use with Claude Desktop, with the
    `<!-- pp-hermes-install-anchor -->` HTML comment marker (U3)

The fixtures use `printing-press-golden` and `printing-press-rich-auth`
as the YAML `author:` value because the golden harness sets both
`github.user` and `user.name` to the same slug-shaped value. Real
publishes resolve OwnerName via `git config user.name` (display name);
the harness's deterministic value is just a fixture detail.

* refactor(cli): collapse three near-identical manifest readers into one helper

readManifestOwner, readManifestOwnerName, and readManifestPressVersion
were 12-line near-clones differing only in the JSON field they
extracted from .printing-press.json. Replace with a single
readManifestField(outputDir, key) helper plus three thin wrappers.

Net: 30 lines collapsed to 12. AGENTS.md "Pre-commit: scan the diff"
rule explicitly calls out near-identical functions as something to
share — three call sites is the threshold.

Also strips two plan-step references ("(post-U2)", "(U6)") from test
comments per AGENTS.md "No dates, incidents, or ticket numbers in
code comments". The surrounding wording carries the same intent.

No behavior change. 825 tests in internal/generator + internal/spec
still pass; go vet clean.

Surfaces from /simplify review.

* fix(cli): soften empty-OwnerName validation to a warning + slug fallback

Generate() previously hard-errored on `spec.OwnerName == ""`. That broke
~50 tests in CI that construct APISpec inline (not via minimalSpec) and
don't set the field, because GitHub Actions runners don't have
`git config user.name` set so the resolution chain returns empty.

The validation was over-strict for a library that's reused by many
callers (tests, mcp-sync, regen-merge — none of which want to plumb
ownership through). Soften to a stderr warning + fallback to the
slug-shaped Owner, so emission is non-empty and visibly degraded
(`author: "trevin-chow"` instead of `author: "Trevin Chow"`).

Production paths (a real `/printing-press` invocation by a user with
git config set) never hit the fallback — their git user.name resolves
correctly. The library-wide sweep tool's per-CLI authorship mapping in
printing-press-library/tools/sweep-frontmatter overrides this code path
entirely. Only failure mode is: a user without git config running a
fresh print, who sees a stderr warning AND a slug-shaped author in
their output. Loud enough to catch.

* feat(cli)!: prefer npx-based installer over raw go install in Prerequisites

Switches the Prerequisites section's install command from a raw
`go install ...@latest` to `npx -y @mvanhorn/printing-press install
<api> --cli-only`, with the raw Go install retained as a documented
fallback.

Why: the `@mvanhorn/printing-press` installer is already shipped as a
centralized install path for these CLIs (the README's `## Use with
Claude Code` section uses the same package for skill installs). Routing
binary installs through it gives us aggregate npmjs download stats —
imperfect but meaningfully better than the zero usage signal raw
`go install` provides today. Same on-disk result, same verification
step, same `$PATH` guidance — just a shorter, more readable command
that flows through tracked install infrastructure.

The npx form takes the api-name (`mercury`) instead of the long module
path (`github.com/mvanhorn/printing-press-library/library/payments/
mercury/cmd/mercury-pp-cli`). The Go fallback below it preserves the
escape hatch for offline environments or systems without Node.

BREAKING CHANGE: Prerequisites section content changes for every
printed CLI on next regen. The library-wide sweep is shipping the
same shape across the existing 49 entries.

Goldens regenerated for both `generate-golden-api` and
`generate-golden-api-rich-auth`. Full test suite (578 generator tests)
passes.

* docs(cli): apply review polish — plan accuracy, AGENTS note, fallback test

Items the full-PR re-review surfaced:

  * Plan doc: 4 references to "fails generation early" / "errors
    clearly" updated to reflect the soft-fallback behavior actually
    landed in ae25d568 (warning + slug fallback, not hard error).
  * Plan doc: 2 references to tools/migrate-skill-metadata updated to
    note the file was retired in U7 of this same plan.
  * AGENTS.md: succinct "Owner (slug) vs OwnerName (display)" note
    added to the existing Naming and Disambiguation section. Captures
    the resolution-path divergence, the YAML-escape requirement, and
    the soft-fallback behavior. Future contributors hit this in
    AGENTS.md before they hit the diff.
  * scripts/golden.sh: one-line comment near the GIT_CONFIG injection
    explaining that the slug-shaped author: field in golden fixtures
    is a deterministic-fixture artifact, not the production shape.
    Pre-empts the "is this a regression?" question on regen.
  * Test: TestGenerateSoftFallsBackOnEmptyOwnerName — covers the
    soft-fallback path explicitly. Asserts no error, OwnerName gets
    populated from Owner, rendered SKILL.md emits author: "<slug>"
    rather than empty.

No behavior change. 579 generator tests pass, go vet clean, goldens
verify cleanly.

* docs(cli): capture 5 learnings from Hermes/OpenClaw frontmatter alignment

Compounds knowledge from this session into docs/solutions/. Each
captures a generalizable pattern, not just the specific fix.

Three learnings about generator/library design (apply repo-wide):

  * design-patterns/dual-key-identity-fields-2026-05-06.md
    Owner (slug) vs OwnerName (display) — the structural pattern of
    keeping path-shaped and prose-shaped representations of one
    conceptual identity as separate fields with separate resolution
    paths. Generalizable beyond the owner case.

  * conventions/soft-validation-in-reusable-library-packages-2026-05-06.md
    When a library package serves many callers (production CLI, tests,
    mcp-sync, regen-merge), hard validation can break the test/tool
    callers without producing correctness value. Stderr warn + degraded-
    but-visible fallback lets the production path catch real
    misconfigurations while keeping the library reusable.

  * design-patterns/avoid-classification-when-failure-is-asymmetric-2026-05-06.md
    The biggest lesson from two rounds of plan review: when a
    classifier's false-positive cost is much greater than its false-
    negative cost, even a 99%-accurate classifier may be wrong to
    ship. Specific application: env-var hoisting in agent-host
    frontmatter; general principle applies anywhere classification
    routes user-visible behavior.

Two learnings clearly noting cross-repo coupling with mvanhorn/
printing-press-library (the lesson lives here because it's our
template/generator changes that introduce the coupling):

  * conventions/preserve-original-authorship-in-multi-author-retrofits-2026-05-06.md
    A sweep tool over multi-author content should source authorship
    from primary evidence (copyright headers, git first-committer),
    not from the operator's git config. Captures the curated-mapping
    approach used in printing-press-library's sweep-frontmatter tool.

  * conventions/external-tool-flags-need-validator-allowlist-2026-05-06.md
    SKILL.md install instructions embed flags from external tools
    (npx, hermes, claude mcp). Doc validators that scope to the file's
    "owning" CLI's source can't tell those apart from real CLI flags.
    Allowlist mechanism + cross-repo coordination required.

  * best-practices/auto-mirror-workflow-trigger-coverage-2026-05-06.md
    Source/mirror invariants where the mirror regenerates on push to
    main but verifies on PR — PR authors need to regenerate locally
    before commit. Documents the upstream-template-change ->
    downstream-mirror coupling.

All five docs follow knowledge-track schema with concrete code
examples, applies_when conditions, and "when not to apply" anti-
patterns. Cross-references thread between them and to existing
auth-envvar-rich-model learning.

AGENTS.md already surfaces docs/solutions/ at line 85; no
discoverability edit needed.

* docs(cli): merge two cross-repo learnings into one coordinated-coupling doc

Replaces:
  - best-practices/auto-mirror-workflow-trigger-coverage-2026-05-06.md
  - conventions/external-tool-flags-need-validator-allowlist-2026-05-06.md

With:
  - best-practices/cross-repo-coordination-with-printing-press-library-2026-05-06.md

Both deleted docs covered the same shape — invisible CI couplings
between cli-printing-press template/schema changes and downstream
printing-press-library workflows. Two specific failure modes (mirror
parity and validator allowlist) are now sections within one doc,
fronted by a pre-flight checklist that future template-change authors
can scan at-merge time.

The merged doc keeps both concrete examples but drops the duplication.
The pre-flight checklist section is the operationally-useful piece that
the original two docs didn't surface explicitly.
2026-05-06 19:23:39 -07:00
Trevin Chow 174d23c8fb fix(cli): add source rate-limit guardrails (#366) 2026-04-28 12:01:10 -07:00
Trevin Chow af21450994 feat(cli): MCPB manifest and bundle support for generated CLIs (#355)
* fix(cli): substitute path placeholders in MCP tool URLs regardless of declared param location

Generated MCP tools were sending literal "{name}" placeholders in URLs
when the spec author declared a path-template parameter as location:query
(or with no location), or when the param was reclassified to a flag for
pagination/date reasons. Affects 49 tool registrations across 5 published
CLIs (pagliacci-pizza 29, cal-com 13, movie-goat 4, flightgoat 2,
company-goat 1).

Two-layer fix:

1. internal/spec/spec.go: enrichEndpointPathParams now promotes existing
   params whose name matches a {placeholder} in the URL path by setting
   PathParam=true and Required=true, instead of skipping them. The path
   template is authoritative — even if the author wrote location:query
   the URL still has {cik} as a substitution slot. PathParam=true
   preserves CLI-rendering intent (flag, not positional arg) while
   making the param visible to URL substitution.

2. internal/generator/templates/mcp_tools.go.tmpl: emit positionalParams
   from any param where Positional || PathParam is true, not just
   Positional. This catches both the parser-promotion case above and the
   OpenAPI parser's existing reclassification path
   (parser.go:1620 sets PathParam=true on date/year/month/pagination
   params already in path), which the template silently ignored.

Tests:
- internal/spec/spec_test.go: TestEnrichEndpointPathParams covers the
  three meaningful cases: undeclared placeholder (existing positional
  injection), declared-as-flag promoted to PathParam, repeated
  placeholder enriched once. Pins body-declared placeholders as
  intentionally not enriched.
- testdata/golden/fixtures/golden-api.yaml: new /reports/{year}/summary
  endpoint exercises the OpenAPI reclassification path (year is in:path
  but the parser promotes to PathParam=true). Golden output's tools.go
  now correctly emits []string{"year"} as positionalParams; without
  the template fix it would be []string{} and the URL would send
  literal {year} to the API.

Goldens updated to reflect the new fixture endpoint. Diffs verified:
- tools.go gains one MCP tool with []string{"year"} substitution list
- root.go gains one cobra command registration
- counts/checksums in .printing-press.json, dogfood.json, scorecard.json
  shift to match the expanded spec (6→7 tools, 35→38 endpoint metrics)
- README/SKILL gain a "reports" section

Codemod for the 5 affected published CLIs follows in a separate change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): emit MCPB manifest.json and friendly-name MCP server identity

Generated CLIs now ship a manifest.json (MCPB v0.3 format) alongside
.printing-press.json. The manifest is what Claude Desktop, Claude Code,
and other MCPB-aware hosts read when installing a .mcpb bundle — it
declares the MCP server identity, the launch command, and any user_config
fields the host should prompt the user for at install time.

Manifest contents are derived entirely from existing per-CLI metadata
(.printing-press.json + spec auth fields + catalog display_name) so the
agent can audit it during Phase 4 shipcheck just like it audits SKILL.md
and README.md. The MCPB bundle ZIP itself is a release-time goreleaser
artifact (attached to GitHub releases); manifest.json is the source of
truth committed to the published CLI directory.

Pieces:

- internal/spec/spec.go: APISpec gains DisplayName field plus
  EffectiveDisplayName() helper that returns the explicit value or
  title-cases Name as a fallback. Used wherever a friendly brand
  identity beats a kebab-case slug — Claude Desktop's connector list,
  manifest display_name, the MCP server's protocol-level name.
- internal/generator/templates/main_mcp.go.tmpl: passes
  EffectiveDisplayName into server.NewMCPServer instead of the
  kebab-case binary name. Connector menus now show "Stripe" or
  "Cal.com" or "PokéAPI" instead of "stripe-pp-mcp".
- internal/pipeline/climanifest.go: CLIManifest gains DisplayName
  (with catalog/spec fallback chain) and AuthKeyURL fields.
  populateMCPMetadata sets both. WriteManifestForGenerate now also
  writes manifest.json.
- internal/pipeline/mcpb_manifest.go (new): MCPBManifest types
  matching the upstream v0.3 schema, plus WriteMCPBManifest that
  reads .printing-press.json and emits manifest.json. Skips when
  MCPReady is "cli-only" (composed-auth flows where MCP can't stand
  alone) or when no MCP binary exists. user_config fields are sensitive
  by default and required only when auth_type gates every API call
  (api_key, bearer_token, oauth2); cookie/composed/none keep them
  optional so agents installing a Claude-Desktop-only bundle aren't
  forced to enter credentials they may not have yet.
- internal/pipeline/publish.go, lock.go: replace writeSmitheryYAML
  calls with WriteMCPBManifest. The smithery.yaml emission was never
  wired up to Smithery's actual registry (which requires per-CLI
  registration on smithery.ai with no Go install path), so the file
  was inert documentation. The MCPB manifest is the path the upstream
  MCP project standardized on (Nov 2025) with growing host adoption.
- internal/pipeline/publish.go: smitheryConfig types and writeSmitheryYAML
  removed. yaml import dropped (no longer used in this file).

Tests:
- TestWriteSmitheryYAML replaced with TestWriteMCPBManifest covering
  the same gates (no-manifest, cli-only skip, missing binary skip)
  plus MCPB-specific cases: api_key required, composed optional,
  multi-env-var (company-goat shape with both GITHUB_TOKEN and
  COMPANIES_HOUSE_API_KEY optional), no-auth-no-user-config.
- testdata/golden/cases/generate-golden-api/artifacts.txt now snapshots
  manifest.json and cmd/<api>-pp-mcp/main.go so the friendly-name flow
  and full manifest shape are locked in as a golden contract.

Out of scope for this commit (will follow):
- Refactoring the context MCP tool template to emit capability-derived
  bullets instead of marketing prose.
- Codemod across the public library (manifest backfill, friendly-name
  update for existing CLIs, goreleaser config for .mcpb bundles).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): MCP context tool delineates MCP-callable vs CLI-only capabilities

The context tool's prior output named hand-written features
"unique_capabilities" without distinguishing whether each item was
actually MCP-callable. Claude Desktop users reading this output have
inferred the MCP could invoke commands like snapshot or funding
directly, when those are CLI-only — leading to attempts that surface
404s or "tool not found" instead of clear "use the CLI binary"
guidance.

Two surface changes in the generated handleContext output:

- Renames "unique_capabilities" → "cli_only_capabilities" so the key
  itself signals the scope. Each entry gets a "via": "cli" field, which
  is machine-readable for agents that filter capabilities by transport.
- Adds a new "tool_surface" field with one sentence explaining that
  resources/sync/search/sql are MCP-callable while
  cli_only_capabilities require running the companion -pp-cli binary.
  Sits next to tool_count so an agent reading top-down sees the
  boundary before parsing capability lists.

Codified in the golden fixture so future template changes can't
silently re-conflate the two surfaces.

Out of scope (deliberate): wholesale separation of research-derived
prose into "positioning" vs "capability" channels. The renaming +
tool_surface note solves the misinterpretation Claude Desktop made in
practice; the broader prose-channel split is a larger refactor that
should follow once we've seen what other prose drift surfaces in real
agent sessions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli): add bundle subcommand and auto-bundle on generate

A printed CLI's manifest.json alone isn't installable — Claude Desktop
takes a .mcpb ZIP, not a JSON file. Personal-use printers (anyone who
generates a CLI for themselves without publishing to the public library)
need a way to package locally without GitHub Actions or hand-rolled bash.

Two surfaces:

1. `printing-press bundle <cli-dir>` — packages an already-generated CLI
   into a .mcpb. Compiles the MCP binary via `go build`, ZIPs with
   manifest.json, writes <dir>/build/<name>-<os>-<arch>.mcpb. Supports
   --platform for cross-compile, --skip-build with --binary for pre-
   built artifacts (so release pipelines that build via goreleaser can
   reuse the packaging step).

2. `printing-press generate` now auto-bundles for the host platform
   after writing manifest.json. Skips silently when go.sum isn't
   populated (e.g. --validate=false leaves the module untidied), so
   personal-use printers get an immediately-installable .mcpb without
   needing to know about the bundle subcommand, while machine flows
   that intentionally skip module wiring don't see noisy warnings.

The `bundle` subcommand exists separately from auto-run so users can:
- Cross-compile to a non-host platform (--platform linux/amd64)
- Rebuild after manually editing manifest.json or the MCP binary
- Wrap a pre-built binary from another build pipeline (--skip-build)
- Recover from an interrupted generate that wrote the manifest but
  failed mid-build

Pieces:

- internal/pipeline/mcpb_bundle.go: BuildMCPBBundle assembles the
  ZIP from a manifest.json and binary path. Preserves POSIX execute
  bits so macOS/Linux hosts can launch directly. DefaultBundleOutputPath
  conventionally writes to <dir>/build/<name>-<os>-<arch>.mcpb.
- internal/pipeline/mcpb_bundle_test.go: covers happy path, missing
  manifest skip, empty entry_point rejection, executable bit preservation.
- internal/cli/bundle.go: cobra command, plus autoBundleForHost helper
  used by generate. resolvePlatform parses <os>/<arch> with a useful
  error rather than silently defaulting on bad input.
- internal/cli/root.go: registers `bundle`, calls autoBundleForHost
  after both generate code paths (docs-driven and spec-driven).

Bundles are gitignored release outputs (~10MB compressed); only
manifest.json is the source of truth committed to the repo. CI/release
pipelines build the cross-platform matrix via goreleaser; the host-only
auto-build in generate covers personal-use install without requiring CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cli): simplify MCPB manifest emission per /simplify pass

Acts on findings from the post-implementation simplify review:

P1 — correctness/UX

- mcpb_manifest.go: Version was hardcoded "1.0.0", which would defeat
  Claude Desktop's "newer bundle available" detection. Now stamps from
  CLIManifest.PrintingPressVersion (falls back to the linker version
  when the manifest field is empty). golden.sh normalizes the stamped
  version to <PRINTING_PRESS_VERSION> in the manifest.json fixture so
  release bumps don't churn the golden.
- spec.go: EffectiveDisplayName's hand-rolled `s[:1]` title-case loop
  sliced mid-codepoint on multi-byte runes (would mangle "PokéAPI",
  "Café"). Now calls a new `naming.HumanName` helper that uses
  `cases.Title(language.English)` — same path generator.go's humanName
  template func and proseName() were already using. Both call sites
  collapsed to the new helper, eliminating two duplicate implementations.
- root.go: autoBundleForHost ran before the "Generated <api> at <path>"
  print, blocking that line behind 5–15s of go build. Moved it after
  the print so users see completion immediately and the bundle output
  follows as a separate "Bundled <path>" line. Same effect on both
  generate code paths (docs-driven and spec-driven).

P2 — cleanup

- mcpb_manifest.go: hoisted "binary", "string", platform list, and the
  authTypeAPIKey/BearerToken/OAuth2 strings to package consts so a
  typo can't silently flip authRequiresCredential's branch.
- climanifest.go: WriteManifestForGenerate now calls
  WriteMCPBManifestFromStruct (new in-memory variant) instead of
  WriteMCPBManifest, which was reading and re-parsing the JSON we'd
  just written. Saves a syscall and prevents drift if the writer/reader
  paths ever diverge. Old WriteMCPBManifest kept for the standalone
  `bundle` subcommand which has nothing in memory.
- mcpb_bundle.go: added pipeline.StagedMCPBinaryPath helper so
  internal/cli/bundle.go doesn't reach into pipeline's directory
  layout. Sentinel error in BuildMCPBBundle uses errors.New instead
  of fmt.Errorf with no formatting.
- bundle.go: trimmed long doc comments on autoBundleForHost. Replaced
  "manifest.json missing — has this CLI been generated yet? %w" (which
  rendered as "...generated yet? open <path>: no such file or directory")
  with "reading manifest.json (run `printing-press generate` first): %w".
- mcpb_manifest.go: dropped envVarTitle (a pure identity function with
  more comment than code) — the title is just the env var verbatim and
  inlines cleanly. Compressed the SetEscapeHTML and skip-cases comments;
  retained the WHY each gate exists.
- mcp_tools.go.tmpl: trimmed the 9-line tool_surface preamble comment
  to 2 lines; the emitted tool_surface field text already explains the
  same thing to the agent reading runtime output.
- spec_test.go: renamed misleading "still triggers promotion path" case
  to "is not promoted" so the name matches what the assertion checks.

Test results unchanged (2330 passing, golden verify clean). Goldens
regenerated for the manifest comment trim and the version stamp
normalization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): bundle observability, streaming binary, claude-desktop version, tests

Acts on PR review P2 findings (deferred #3 go-build consolidation
— that's an /simplify-and-refactor sweep across 11 call sites and
out of scope here).

#1 — autoBundleForHost error reporting

Distinguish expected silent skips (no manifest, no go.sum — both mean
"this CLI doesn't ship a bundle today, that's fine") from real
failures (malformed manifest, empty manifest.name — both mean a
partial-write/corruption bug the user wants to hear about).
Previously every error path was silent, hiding broken state.

#2 — stream binary instead of buffering in RAM

BuildMCPBBundle now opens the MCP binary and io.Copy()s it into the
zip stream. Old path read the entire binary (~10-15 MB for our
Go binaries) into a []byte before zipping, doubling peak resident
memory for no benefit. Added writeZipReader (io.Reader-shaped)
alongside writeZipBytes so manifestData (small, in memory) can keep
its existing call site without a wasteful bytes.Buffer round-trip.

#5 — minClaudeDesktopVersion constant

Hoisted ">=1.0.0" out of buildMCPBManifest into a named const so
the version sits next to the comment that explains why it's that
value (the MCPB-introducing release of Claude Desktop). Bumping later
becomes a one-line change instead of a hunt across templates.

#4 — tests for bundle CLI surface

New internal/cli/bundle_test.go covers what the pipeline-level tests
don't:
- TestResolvePlatform: empty/host fallback, "<os>/<arch>" parsing,
  malformed-input errors. Pure function, exhaustive.
- TestAutoBundleForHost: no manifest (silent), malformed manifest
  (loud), empty name (loud), missing go.sum (silent). Pins the
  observability boundary #1 establishes.
- TestNewBundleCmdMissingManifest: cobra-level error wrapping —
  asserts the user-facing message hints at running `generate` first.
- TestNewBundleCmdNotADirectory: cobra-level path validation.

12 new test cases total; suite is now 2342 passing (was 2330).
buildMCPBBinary itself isn't unit-tested — it shells out to `go build`
and the existing TestBuildMCPBBundle already exercises the
"binary-on-disk → bundle" half. Integration coverage of the full
generate→bundle path lives in the golden suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:06:28 -07:00
Trevin Chow 907c6986b5 feat(cli): add unknown-command check + sync test for verify-skill (#339)
The bundled verify-skill script (internal/cli/verify_skill_bundled.py)
that ships in the printing-press binary was running 3 checks: flag-names,
flag-commands, positional-args. The library repo's CI runs a fork that
adds an unknown-command check — every command path referenced in SKILL.md
must map to a real cobra Use: declaration. That fork rejected the dub
PR (mvanhorn/printing-press-library#135) for phantom paths like `qr
get-qrcode` while local Phase 4 reported PASS. Refs issue #336 (U2).

The two scripts in this repo (scripts/verify-skill/verify_skill.py and
internal/cli/verify_skill_bundled.py) were already byte-identical at HEAD
— both 785 lines, neither implementing unknown-command. The retro's "200
lines drift" was a snapshot at a past commit; whatever drift existed had
been manually reconciled. So this PR has two parts:

1. Port the unknown-command check.

   The upstream library-repo script and this repo's canonical have
   diverged ~1,400 lines — they're forks with different helper graphs.
   A verbatim lift would not work. The new check is re-derived using
   this repo's existing find_command_source helper (which walks the
   rootCmd.AddCommand graph and resolves multi-level paths cleanly,
   richer than upstream's). It surfaces command paths from two surfaces:

     - Bash recipes (already extracted by extract_recipes for the other
       checks; previously dropped silently when the command was missing)
     - Inline backtick references inside the `## Command Reference`
       section (scoped to that section to avoid false positives in
       narrative prose)

   Each unique path is reported at most once. Cobra's auto-registered
   builtins (help, completion, version) are whitelisted.

   Updates to scripts/verify-skill/verify_skill.py:
     - Add COMMAND_REFERENCE_SECTION_RE and BUILTIN_COMMANDS constants
     - Add _extract_inline_commands helper
     - Add check_unknown_commands function
     - Wire into run_checks dispatcher and --only argparse choices
     - Update header docstring (4 checks now)

2. Drift prevention.

   internal/cli/verify_skill_sync_test.go adds TestVerifySkillScriptInSync
   which sha256-hashes both files and fails if they differ, with a clear
   error pointing the developer at the canonical as source-of-truth and
   the lefthook command to regenerate.

   lefthook.yml gains a pre-commit `sync-verify-skill` hook that copies
   canonical → bundled and re-stages the bundled file. Mirrors the
   existing fmt block's `stage_fixed: true` shape. The hook prevents
   drift on the happy path; the Go sync test catches `--no-verify`.

Three new integration tests in
internal/cli/verify_skill_unknown_command_test.go:
  - TestVerifySkill_DetectsUnknownCommand — phantom path → exit 1 +
    "[unknown-command]" + "closest existing prefix is `<cli> qr`"
  - TestVerifySkill_UnknownCommandPassesWhenAllPathsResolve — clean
    SKILL passes
  - TestVerifySkill_UnknownCommandSkipsBuiltins — help/completion/version
    not flagged

Validation against existing library CLIs found real findings on
allrecipes (4 phantom paths: category, cuisine, ingredient, occasion)
and cal-com (3 phantom paths: api-keys/keys-refresh, auth/oauth2-*).
These are pre-existing bugs that the new check will surface on next
regeneration. The dub library copy passes (was hand-fixed in PR #135).

Refs #336, #333
2026-04-27 02:35:46 -07:00
Trevin Chow 0a1e80d9da test(cli): add golden output harness (#310)
* test(cli): add golden output harness

* test(cli): strengthen golden artifact coverage

* test(cli): preserve raw golden Go artifacts

* test(cli): pin golden generation owner

* test(cli): snapshot golden verification outputs
2026-04-26 13:21:04 -07:00
Trevin Chow 8aad8d858b fix(cli): retro #301 — six improvements from recipe-goat regenerate (#303)
* fix(cli): skip flag-descriptor tokens in positional-arg inference

`Use` strings of the form "save <url> [--tags=<csv>] [--stdin]" were
leaking `<csv>` as a phantom positional via inferPositionalArgs, which
then breaks `cobra.MaximumNArgs(1)` validators on commands that accept
exactly one real positional. The placeholder regex couldn't tell
`[--tags=<csv>]` (a flag descriptor) apart from `[id]` (an optional
positional).

Refactor placeholder extraction into a pure function and prefilter
bracketed tokens whose body starts with leading dashes or contains `=`
before the placeholder regex runs. Tested across simple positionals,
flag descriptors with embedded placeholders, short-form flags, and
spaced flag bodies.

Surfaced during the recipe-goat regenerate (PR mvanhorn/printing-press-library#127); part of retro #301 finding F2.

* fix(cli): walk AddCommand graph instead of guessing via Use-string specificity

The verify-skill flag-commands check was producing false-positive
"--<flag> is declared elsewhere but not on <command>" findings whenever
two cobra commands at different paths shared a leaf name (e.g.,
`recipe-goat-pp-cli save <url>` plus `recipe-goat-pp-cli profile save
<name>`). The old find_command_source picked candidates by counting
positional/optional/variadic tokens in the `Use:` string and returned
only the highest-specificity tier — which dropped the lower-specificity
file from the flag-declaration union, causing real flags on the dropped
command to be reported as missing.

Replace the specificity heuristic with a walk of `rootCmd.AddCommand`
calls plus the `<parent>.AddCommand(newXxxCmd(...))` chain inside each
constructor function. The graph walk picks exactly one file per command
path with no false positives. Falls back to the legacy specificity
behavior for unconventional CLIs that don't follow the standard
`func newXxxCmd` factory pattern.

Sync internal/cli/verify_skill_bundled.py from the canonical
scripts/verify-skill/verify_skill.py. Add a Go regression test
(TestVerifySkill_NoFalsePositiveOnSharedLeafName) that materializes a
synthetic CLI with the F1 shape and asserts "All checks passed". Add
Python unit tests for collect_command_constructors, find_root_children,
resolve_command_path, find_command_source's new path, and
_extract_function_body's brace-tracking through strings/comments.

Surfaced during the recipe-goat regenerate (PR
mvanhorn/printing-press-library#127); part of retro #301 finding F1.

* fix(cli): carve out novel-static-reference commands from reimplementation check

Novel features that ship curated static data (substitution tables,
holiday lists, currency metadata, conversion factors) have no API
client call and no store call by design — the data IS the feature.
The dogfood reimplementation_check was flagging these as
"hand-rolled response: no API client call, no store access" because
its only carve-outs were store-signal and client-signal.

Add a third carve-out: a per-command opt-out marker
`// pp:novel-static-reference` anywhere in the file. When the marker
is present, classifyReimplementation exempts the command on the same
footing as the store/client carve-outs.

Tests verify both directions:
- TestCheckReimplementation_NovelStaticReferenceMarker_Exempted: file
  with the marker is exempted even with no client/store signals.
- TestCheckReimplementation_WithoutMarker_StillFlagged: same file shape
  without the marker is still flagged with the existing reason.

Document the new annotation in AGENTS.md alongside the existing
Anti-Reimplementation block.

Surfaced during the recipe-goat regenerate (PR
mvanhorn/printing-press-library#127); part of retro #301 finding F3.

* feat(cli): add cliutil.ProbeReachable helper for multi-source health checks

Multi-source CLIs (recipe-goat, movie-goat, contact-goat, weather-goat,
flightgoat) keep rediscovering that some CDN-fronted hosts (BBC,
RecipeTin Eats, AllRecipes, Serious Eats, The Kitchn) terminate HEAD
requests with TLS shutdown / EOF even though they serve GET cleanly.
Authors hand-roll HEAD-based probes that lie — reporting unreachable
for hosts the real fetch path is successfully scraping. recipe-goat
hit this directly: doctor reported six sites unreachable that the goat
ranker was using fine.

Add a generator template emitting internal/cliutil/probe.go in every
printed CLI:

  ProbeReachable(ctx, client, url) (status, code int, err error)

Implementation: GET with Range: bytes=0-1023, drain up to 2 KiB into
io.Discard, classify the result.

  - 200/206 → reachable (host honors Range, full or partial body)
  - 416 → reachable (host doesn't support Range, but headers came back
    so the host is up)
  - other 4xx/5xx → blocked (host is up but refusing this request)
  - network-layer failures → unreachable

Doctor commands doing per-source fan-out should call ProbeReachable
with the same client used by the real fetch path so probe drift can't
recur.

Tests cover all five status paths plus the nil-client fallback and a
header-sent assertion. Verified end-to-end by generating a synthetic
CLI from a probe-enabled spec and running the emitted cliutil tests.

Surfaced during the recipe-goat regenerate (PR
mvanhorn/printing-press-library#127); part of retro #301 finding F4.

* docs(skills): re-validate prior research after machine upgrades

When the user picks 'Generate a fresh CLI' on an existing library
entry whose .printing-press.json was stamped by an older binary
(differing minor or major version), prompt once before kicking off
Phase 1 research. The prompt names the high-impact machine deltas
that commonly invalidate prior briefs (Surf-Chrome HTTP transport,
MCP intent tools, scoring rubrics, auth modes) and asks whether to
re-validate or reuse the prior brief verbatim. Skips on patch deltas,
same-version regenerations, and first generations.

Recipe-goat hit this directly: the 2026-04-13 brief framed
reachability as Tier 1/2/3 — a pre-Surf classification that became
obsolete when the binary gained Surf-Chrome impersonation in 2.x.
The user had to manually prompt 'validate Tier 2/3 status with surf'
mid-Phase-4 to catch it; with this guardrail, the skill prompts at
the start instead.

Surfaced during the recipe-goat regenerate (PR
mvanhorn/printing-press-library#127); part of retro #301 finding F5.

* docs(skills): warn against hardcoded runtime-state counts in narrative

Add rule 11 to the Phase 1 narrative-block authoring rules: avoid
hardcoded counts in headline / value_prop when the count tracks a
runtime list (Sites slice, retailer list, league list, vendor list).
A phrase like 'across 15 trusted sites' embeds the integer into
root.go's Short/Long, README, SKILL.md, the MCP tools description,
and which.go simultaneously — and when the underlying registry grows,
a single-line addition leaves ~10 hardcoded copies stale.

Prefer plural-without-count phrasing ('across the major recipe
sites'), a qualitative descriptor ('dozens of vendors'), or — if a
count genuinely matters — a {{ N }} placeholder with a human-facing
comment naming which runtime list it tracks.

Recipe-goat's regenerate ended up fixing 'across 15 trusted sites' →
'across 37 trusted sites' in 11 places (root.go Short, root.go Long
banner, root.go highlights bullet, README, SKILL line 11, SKILL line
133, .goreleaser.yaml, spec.yaml, tools-manifest.json, MCP tools.go,
agent_context.go, which.go) — work the original brief could have
avoided by writing a count-free narrative.

Surfaced during the recipe-goat regenerate (PR
mvanhorn/printing-press-library#127); part of retro #301 finding F6.
2026-04-26 02:22:31 -07:00
Trevin Chow e741db8075 feat(cli): generate replayable website CLIs (#241)
* feat(cli): analyze replayable browser-sniff traffic

Add traffic analysis and reachability classification for browser-sniff
captures, including GraphQL BFF expansion and replayable HTML surface
inference. Extend spec validation so generated specs can represent the
replayable surfaces discovered from browser traffic.

* feat(cli): generate replayable website CLIs

Teach the generator to emit lightweight browser-compatible HTTP clients,
HTML extraction helpers, browser-assisted auth refresh support, and more
semantic command surfaces for sniffed website APIs without shipping a
persistent browser sidecar transport.

* fix(cli): score browser-derived CLIs accurately

Update dogfood, verify, live-check, tools-manifest, and scorecard logic for
browser-sniffed website CLIs. Keep structural checks focused on replayable
API surfaces and credit live verification from generated command trees when
novel features have been absorbed by the generator.

* fix(skills): clarify browser discovery consent

Document browser-sniffing as replayability discovery rather than a promise
that every captured page-context route will ship. Tighten challenge/HAR
fallback guidance and keep skill verification aligned with generated command
naming.

* fix(cli): harden generated docs and validation gates

* fix(skills): tighten printing press generation guidance

* feat(cli): add Product Hunt to the catalog

Catalog Product Hunt as a community marketing entry backed by a custom website-surface spec, and allow custom spec formats for non-OpenAPI catalog seeds.

* fix(ci): validate in-repo catalog specs from PRs

* fix(ci): keep catalog validation targeted
2026-04-23 02:08:11 -07:00
Trevin Chow 297c3c14f2 feat(scripts): add verify-skill — static SKILL.md validator (#194) 2026-04-12 19:02:33 -07:00