mirror of
https://github.com/mvanhorn/cli-printing-press.git
synced 2026-09-14 15:38:08 +08:00
tmchow/openclaw-bin-dir-install
27 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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
|
||
|
|
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
|
||
|
|
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> |
||
|
|
fc04a3e6f2 |
fix(ci): add generated output compile gate (#2319)
* ci(ci): add generated output compile gate * fix(ci): address generated output review feedback |
||
|
|
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> |
||
|
|
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> |
||
|
|
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 |
||
|
|
a1b96ff74d | feat(installer): add repo bootstrap script (#2044) | ||
|
|
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 |
||
|
|
d328375cf1 |
fix(cli): distinguish verify-skill prose invocations (#1906)
* fix(cli): distinguish verify-skill prose invocations * fix(cli): ignore prose separator flags |
||
|
|
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 |
||
|
|
595ed9ec92 | fix(cli): require documented flags in verify-skill (#1707) | ||
|
|
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>
|
||
|
|
c3e1b87aa9 | feat(cli): emit .printing-press-patches.json on every generate (#1590) | ||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
d0086ffe38 | fix(cli): handle Windows shipcheck paths (#783) | ||
|
|
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>
|
||
|
|
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
|
||
|
|
174d23c8fb | fix(cli): add source rate-limit guardrails (#366) | ||
|
|
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>
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
297c3c14f2 | feat(scripts): add verify-skill — static SKILL.md validator (#194) |