{"slug":"cli-printing-press","total":1053,"limit":100,"offset":0,"since":null,"commits":[{"hash":"aecf495a","date":"2026-05-19 22:41:12 -0700","author":"Trevin Chow","subject":"fix(cli): harden manifest-gen remote spec loader against hangs and silent truncation (#1699)","body":"* fix(cli): harden manifest-gen remote spec loader against hangs and silent truncation\n\nloadSpec in cmd/manifest-gen had two defects when fetching remote specs:\n\n1. No timeout on the HTTP request. http.Get with the default client has no\n   overall request timeout, so a remote host that flushes headers and then\n   blocks on the body would hang manifest-gen indefinitely.\n\n2. Silent truncation. io.LimitReader(body, 50MiB) followed by io.ReadAll\n   returned the first 50 MiB of any oversized response with no error, so the\n   caller could not tell a real 49 MiB spec from a multi-gigabyte stream\n   truncated mid-document.\n\nFix: route the request through http.NewRequestWithContext with a 60s timeout,\nand read limit+1 bytes so we can return an explicit \"spec exceeds N bytes\"\nerror when the response is over the limit. The 50 MiB cap is preserved.\n\nAdds main_test.go covering happy path, local file, non-200, stalled server\n(with a short timeout override so the test runs in well under a second),\noversized response, and exactly-at-limit response.\n\n* fix(cli): eliminate data race in manifest-gen loadSpec tests\n\nThe previous test helpers `withTimeout` / `withMaxBytes` mutated the\npackage-level `remoteSpecTimeout` and `maxRemoteSpecBytes` vars while\nthe `httptest.Server` handler goroutine read those same vars to size\nits response. There was no happens-before relationship between the\ntest-goroutine write and the handler-goroutine read, so `go test\n-race ./cmd/manifest-gen/...` could flag a data race.\n\nRefactor `loadSpec` to take its byte limit and timeout as explicit\nparameters. The single production caller in `main()` passes\n`maxRemoteSpecBytes` and `remoteSpecTimeout` directly; tests pass\ntheir own local values. No package-level state is mutated, so the\nhandler closures close over local vars and the race is gone by\nconstruction.\n\nRestore the package-level identifiers to `const` since tests no\nlonger override them. Production behavior is unchanged (same 50 MiB\nlimit, same 60s timeout)."},{"hash":"9eff1e20","date":"2026-05-19 19:01:38 -0700","author":"Trevin Chow","subject":"fix(cli): resolve shipcheck CLI binary name from .printing-press.json (#1700)","body":"* fix(cli): resolve shipcheck CLI binary name from .printing-press.json\n\nshipcheckCLIPath and shipcheckCLIPathForGOOS derived the binary name from\nfilepath.Base(o.dir), which is wrong for slug-keyed library dirs. For\n~/printing-press/library/notion, basename is \"notion\" but the binary is\n\"notion-pp-cli\", so validate-narrative was invoked with a binary path\nthat does not exist on disk.\n\nResolve the binary name from .printing-press.json's cli_name via\npipeline.ReadCLIBinaryName, and fall back to filepath.Base(dir) when the\nmanifest is absent or unparseable (preserves legacy behavior).\n\nClawpatch finding: fnd_sig-feat-cli-command-e17f734225-_8ca7979519\n\n* test(cli): cover unparseable-manifest fallback in shipcheckBinaryName\n\nAdds TestShipcheckCLIPath_FallsBackOnUnparseableManifest to pin the\nparse-error branch of pipeline.ReadCLIBinaryName: when\n.printing-press.json exists but is malformed JSON, the binary name\nmust fall back to filepath.Base(dir) rather than propagate the parse\nerror or yield an empty name.\n\nGreptile P2 on #1700 — the godoc promised the fallback for the\n\"absent or unparseable\" case but only the absent path was tested."},{"hash":"b534884f","date":"2026-05-19 19:01:32 -0700","author":"Trevin Chow","subject":"feat(cli): GraphQL credential probe in doctor + actionable copy (#1701)","body":"* feat(cli): verify GraphQL credentials in doctor + actionable unverified copy\n\nThe doctor Credentials check emitted `WARN Credentials: present (not\nverified — set auth.verify_path in spec ...)`. Two problems: the operator\nrunning the CLI doesn't own the spec, and there was no next step. GraphQL\nAPIs (Linear, GitHub, Shopify) also couldn't declare a probe at all since\nthere's no REST verify endpoint to point at.\n\n- Add `auth.verify_query` spec field. When set, doctor POSTs\n  `{\"query\": \"<value>\"}` to base_url and treats HTTP 2xx + no top-level\n  `errors` as verified, 401/403 as rejected. Routed through\n  PostQueryWithParamsAndHeaders so verify-mode doesn't suppress the read.\n  VerifyPath wins when both are set (REST probe is cheaper).\n- Replace the unverified-state copy with a runnable suggestion resolved at\n  doctor-time: walk the cobra tree for the first endpoint-mirror leaf with a\n  list/get verb and no positional args. Gated on the pp:endpoint annotation\n  so it never suggests local-read framework commands, and recurses through\n  Hidden resource parents (whose endpoint leaves stay runnable).\n- Downgrade the unverified state WARN -> INFO; \"we didn't check\" is not a\n  warning and shouldn't render yellow in CI dashboards.\n\nAdditive: behavior for CLIs that already set auth.verify_path is unchanged.\n\n* fix(cli): split 403 from 401 in doctor GraphQL credential probe\n\nThe GraphQL probe collapsed HTTP 401 and 403 into \"invalid — check your\ncredentials\". 403 means a valid-but-scope-limited token, so that copy told\noperators to replace a working token when they only needed to add scopes.\nMirror the REST probe's split: 401 -> invalid, 403 -> scope-limited."},{"hash":"5261ae73","date":"2026-05-19 16:09:14 -0700","author":"Trevin Chow","subject":"fix(cli): return failure exit from verify json (#1693)","body":"* fix(cli): return failure exit from verify json\n\n* fix(cli): preserve verify text failure exit\n\n* test(cli): assert verify json failure exit\n\n* fix(cli): silence verify json failures at root"},{"hash":"4093ef3e","date":"2026-05-19 15:28:19 -0700","author":"Trevin Chow","subject":"fix(cli): redact shipcheck api key in json (#1673)","body":"* fix(cli): redact shipcheck api key in json\n\n* fix(cli): address shipcheck redaction review"},{"hash":"d8178077","date":"2026-05-20 00:23:56 +0200","author":"andreasvainio","subject":"fix(cli): handle binary-only response endpoints (Accept + base64 envelope) (#1574)","body":"* fix(cli): handle binary-only response endpoints (Accept + base64 envelope)\n\nGenerated CLIs hardcoded `Accept: application/json` in the HTTP client\nwhen no per-endpoint override was set, and ran every response through\nthe JSON sanitizer before returning it as `json.RawMessage`. Endpoints\nwhose only 2xx content type is non-JSON (e.g. `application/octet-stream`\nPDF/file downloads) were therefore rejected by the server with HTTP 406,\nand even with the right Accept the bytes would be mangled by the\nsanitizer. This is a generic generator gap: it hits any spec with a\nbinary-only success response.\n\nFix, using existing plumbing with minimal surface:\n\n- openapi parser: detect success responses whose media types are all\n  non-JSON / non-`*/*` / non-`+json` and pin `Accept` to that type via\n  the existing per-endpoint header-override path. New `upsertHeaderOverride`\n  (and a merge in `applyHeaderOverrides`) keeps the Accept override stable\n  when configured per-endpoint headers also apply. JSON and `*/*`\n  endpoints are untouched.\n- client.go.tmpl: content-type-gated base64 envelope\n  (`{\"_pp_binary\":true,...,\"data\":\"<b64>\"}`) for genuinely binary\n  success bodies so they survive the json.RawMessage contract. JSON,\n  `*/*`, XML and every text/* type (incl. text/html, so\n  response_format:html CLIs are unaffected) pass through unchanged. The\n  error path is byte-identical (still sanitized + truncated).\n- command_promoted.go.tmpl: promoted (top-level) GET commands now pass\n  per-endpoint header overrides, matching typed commands. Without this\n  the Accept override never reached the client for promoted commands.\n\nGolden: extended testdata/golden/fixtures/golden-api.yaml with one\nbinary-only endpoint per docs/GOLDEN.md (general machine behavior),\nfroze its generated command file, and regenerated affected expected\nfixtures. All 17 golden cases pass; every diff is attributable to the\nenvelope path or the one added endpoint.\n\nVerification: go build ./..., go vet ./... (changed pkgs), go test ./...\n(0 failures), 5 new parser tests, scripts/golden.sh verify (17/17).\nLive-verified against a real binary endpoint: `Accept: application/json`\nreproduces HTTP 406; `Accept: application/octet-stream` returns the\nfile (200, correct content-type).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>\n\n* fix(cli): thread HeaderOverrides through MCP code-orchestration execute\n\nThe original binary-response fix covered typed CLI commands and promoted\ncommands, but the code-orchestration MCP path (<api>_execute) builds\nrequests from a slim endpoint table and called c.Get/Post/... directly,\nso the per-endpoint Accept override never reached binary-only endpoints\n— <api>_execute on a PDF/octet-stream endpoint still 406'd while the CLI\nworked.\n\nAdd HeaderOverrides to codeOrchEndpoint, emit it in the generated\nregistry from endpoint.HeaderOverrides, and dispatch through\nc.*WithHeaders when present. The client's content-type-gated base64\nenvelope (already in this branch) then handles the binary body.\n\nGolden: generate-mcp-api/code_orch.go updated (struct field + WithHeaders\ndispatch; table emission inert when no endpoint has overrides).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>\n\n* fix(cli): thread header overrides through typed MCP tools\n\n* fix(cli): avoid text response Accept overrides\n\n---------\n\nCo-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>\nCo-authored-by: Trevin Chow <trevin@trevinchow.com>"},{"hash":"1a243810","date":"2026-05-19 17:20:45 -0500","author":"HotFix Ops","subject":"feat(cli): add Quo (formerly OpenPhone) catalog entry and OpenAPI spec (#1597)","body":"Add catalog/quo.yaml plus an in-repo OpenAPI spec for Quo (formerly\nOpenPhone), so the Printing Press can generate a printed CLI for the\nQuo business-phone API.\n\nSpec covers 18 operations across 14 paths in 6 resource groups:\ncontacts, conversations, phone-numbers, users, messages, calls. Auth\nis via a plain API key in the Authorization header (non-Bearer),\ncaptured in the spec's securitySchemes block.\n\nProvenance:\n- tier: community. Quo does not publish a vendor OpenAPI document,\n  so this spec is community-curated from their public docs. Matches\n  the kayak / plaid / telegram / sentry pattern.\n- info.x-spec-provenance records source: official-docs, the docs URL,\n  and a confirmed_quirks list. Every path is derived from the\n  official Quo API reference and cross-verified by live testing\n  against api.openphone.com/v1. No endpoint relies on MCP tool\n  schemas or undocumented probing as its sole source of truth.\n\nQuo API quirks captured in x-spec-provenance.confirmed_quirks and\ninline schema descriptions:\n- POST /messages: 'to' is an array of E.164 strings on both the\n  request body and the Message response schema. The API rejects a\n  bare string and returns 'to' as an array even for single-recipient\n  messages.\n- POST /messages: the request body uses 'content' for the message\n  text while the Message response returns the same text under 'body'.\n  Real Quo API naming asymmetry, not a curator typo.\n\nPOST /messages requestBody wraps the schema in oneOf so the parser\nemits a --body-json fallback (and a body_json MCP input) instead of\nper-field typed flags. The generator's typed body path serializes\narray fields as JSON-strings, which the Quo API rejects; the\n--body-json surface lets agents and humans send the array verbatim\n(\"to\": [\"+155...\"]) without round-tripping through a string.\nVerified by regenerating and reading\ninternal/cli/messages_send.go + internal/mcp/tools.go.\n\ntestdata/golden/expected/catalog-list/stdout.txt: insert the new Quo\nrow in social-and-messaging (alphabetical between front and telegram).\n\nNo generator code, template, scorer, MCP, or pipeline code changes.\n\nVerification:\n- go build -o ./printing-press ./cmd/printing-press\n- go test ./... (all packages pass)\n- go fmt ./... (no Go files touched)\n- go vet ./... (clean)\n- scripts/golden.sh verify (20/20 cases pass)\n- ./printing-press catalog show quo renders the new tier, provenance,\n  and notes\n- ./printing-press catalog list places Quo in\n  social-and-messaging between front and telegram\n- ./printing-press generate --spec catalog/specs/quo-spec.yaml emits\n  the expected --body-json fallback warning for POST /messages and\n  produces a buildable printed CLI that PASSes go mod tidy,\n  govulncheck, go vet, go build, --help, version, and doctor.\n\nCloses #782 (superseded by #1597).\n\nCo-authored-by: ninjaforhire <andrew@mightyphotobooths.com>"},{"hash":"015478bf","date":"2026-05-19 17:12:40 -0500","author":"Nick Walker","subject":"feat(cli): detect Auth0 SPA in-memory auth + emit CDP extractor (#1645)","body":"* feat(cli): detect Auth0 SPA in-memory auth at sniff time and emit CDP extractor\n\nSome sites (Auth0 SPA SDK v2+ with cacheLocation: memory) keep the access\ntoken in JS heap and never write it to cookies or localStorage. The\ncookie-jar extractor in auth login --chrome has no path to those tokens —\nDevTools paste was the only way to land them in config until WU-3a (#1641)\nadded auth set-token.\n\nThis change closes the automation gap. Sniff-time detection looks for\n/oauth/token responses that carry access_token in the body without a\nJWT-shaped Set-Cookie on the same response, and annotates the spec with\nauth.subtype: auth0_spa_in_memory. The generator routes that subtype to\nauth_browser.go.tmpl, which emits an --auth0-spa flag on auth login --chrome.\nThe CDP path attaches to a running Chrome at --debug-port, installs a\nFetch.enable interceptor via chromedp, and captures the next outbound\nAuthorization: Bearer JWT — validated via cliutil.LooksLikeJWT before\nSaveTokens.\n\nSide-effect-command floor applies: the CDP path short-circuits when\ncliutil.IsVerifyEnv() or cliutil.IsDogfoodEnv() is set, so the verifier\nmatrix and dogfood --live runs never attempt a real Chrome attach.\n\nRefs #1598 (WU-3b).\n\n* fix(cli): reset auth0 spa token expiry\n\n---------\n\nCo-authored-by: Cathryn Lavery <cathryn@bestself.co>"},{"hash":"75e4d1b9","date":"2026-05-19 17:12:32 -0500","author":"JustSpellJ","subject":"fix(cli): verify-mode HTTP-verb short-circuit + N1 envelope + doctor + read-only POST bypass (#1398)","body":"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.\n\n- **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.\n- **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.\n- **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.\n- **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.\n- **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.\n- **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.\n- **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.\n- **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.\n- **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.\n\nVerified 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.\n\nCo-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"c5d86825","date":"2026-05-19 15:07:15 -0700","author":"Trevin Chow","subject":"fix(cli): create llm prompt temp files privately (#1674)","body":""},{"hash":"9a474335","date":"2026-05-19 14:53:31 -0700","author":"Trevin Chow","subject":"fix(ci): trust queued Greptile gate","body":""},{"hash":"96bc822e","date":"2026-05-19 14:36:25 -0700","author":"Trevin Chow","subject":"fix(ci): allow queue drafts to skip Greptile","body":""},{"hash":"019dcf3b","date":"2026-05-19 12:05:20 -0700","author":"Trevin Chow","subject":"chore(ci): enable parallel queue checks in Mergify (#1668)","body":"* chore(ci): enable parallel queue checks in Mergify\n\nRaise `merge_queue.max_parallel_checks` from 1 to 3 and split\n`queue_conditions` / `merge_conditions` so Mergify can speculate on\nmultiple queued PRs in parallel via `mergify/merge-queue/*` draft PRs.\n\nThe prior config pinned both `batch_size` and `max_parallel_checks` to\n1 under the belief that both were paywalled features on the OSS plan.\nMergify support clarified (2026-05-19) that only `batch_size > 1`\ntriggers the paid \"Merge Queue Batch\" feature; `max_parallel_checks`\nis a separate, free knob. `batch_size` stays at 1.\n\nGreptile compatibility, which was the secondary reason for the old\npinning, is handled by the new split:\n\n- `queue_conditions` (gates entry on the source PR) keeps the Greptile\n  + `#review-threads-unresolved = 0` requirements.\n- `merge_conditions` (gates the merge after the speculative draft PR's\n  CI passes) requires only the CI checks that actually run on bot-owned\n  branches. Greptile silent-skips bot branches by design, so requiring\n  it here would block every queued PR.\n- `merge_protections` continues to re-enforce Greptile + review-threads\n  on the source PR before the GitHub merge, as belt-and-suspenders.\n\n* docs(ci): capture mergify parallel-checks learning under docs/solutions/\n\nAdd a knowledge-track learning doc that explains the corrected mental\nmodel for Mergify on the OSS plan: `batch_size > 1` is paywalled (the\n\"Merge Queue Batch\" feature), `max_parallel_checks > 1` is not, and\nthe three-block (queue_conditions / merge_conditions / merge_protections)\npattern is what makes draft_pr mode safe alongside Greptile, which\nsilent-skips bot branches by design.\n\nCross-references the wrong-turn history (#1306 raise, #1321 revert,\n#1336 single-block narrowing) and the present fix (#1668) so the doc\nis useful both as forward guidance and as context for why the prior\nconfig was conservative.\n\n* fix(ci): drop pr-title from mergify merge_conditions\n\nGreptile flagged this as a P1 on PR #1668: `.github/workflows/pr-title.yml`\nskips semantic validation only for titles starting with `merge queue:`\n(the batch-mode prefix from #1306). In `draft_pr` mode with `batch_size: 1`,\nMergify's draft PR title format is not guaranteed to match that prefix,\nso requiring `check-success = pr-title` on the draft could stall every\nqueued PR.\n\nThe source PR's title is already validated at `queue_conditions` entry\nand cannot change while queued, so re-checking it on the speculative\ndraft is redundant. Drop it from `merge_conditions` and document the\ntrap in the comment block so the next person editing this config sees\nwhy the omission is intentional.\n\nAlso update the learning doc with this as a third non-obvious failure\nmode under \"Why This Matters\"."},{"hash":"81f07e53","date":"2026-05-19 11:13:44 -0700","author":"Trevin Chow","subject":"feat(skills): check public library for existing CLI before generating (#1650)","body":"* feat(skills): check public library for existing CLI before generating\n\nCatches the case where a user kicks off /printing-press for a CLI that\nalready exists in the public library before Phase 1 research burns\n30-60 minutes. The new gate sits in Phase 0 alongside the local-library\ncheck (mutually exclusive, never double-prompts) and reads\nmvanhorn/printing-press-library/registry.json via gh api.\n\nMatching is agent-driven over the registry, not string-match, so\nsemantic input like \"Hacker News reader\" finds hackernews, \"Cal.com\"\nfinds cal-com, and \"prediction market\" surfaces kalshi. High-confidence\nhits route the user to /printing-press-reprint; medium-confidence hits\npresent alternatives; low confidence is skipped silently to keep the\ngate from becoming a dismiss-me prompt. Fails open on network errors.\n\n* Address PR review feedback (#1650)\n\n- Multi-High match now has its own prompt with correct framing\n  (same product under different names, not \"similar/may overlap\")\n  and explicit cap-at-2 with truncation guidance for 3+ candidates\n- Tempfile cleanup guarded with [ -n \"$REGISTRY\" ] && rm -f\n  so the trailing cleanup doesn't run rm -f \"\" when the fetch\n  branch already cleaned up and emptied $REGISTRY\n\n* Address PR review feedback (#1650)\n\n- Specify Combo CLIs prompt concretely with template, cap rule, and\n  confidence tagging. Cap displayed reprint options at 2 across all\n  sources combined to stay under the AskUserQuestion 4-option limit;\n  ordering rule (High over Medium > primary over secondary > canonical\n  slug); truncation note for >2 matches. Continue-with-combo becomes\n  the recommended default since combo CLIs are usually informational.\n\n* fix(skills): make combo library check reachable\n\n---------\n\nCo-authored-by: Cathryn Lavery <cathryn@bestself.co>\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"cda9b43b","date":"2026-05-19 08:44:05 -0700","author":"Trevin Chow","subject":"fix(cli): make live-dogfood runner enum-aware and resilient to empty outcomes (#1656)","body":"* fix(cli): make live-dogfood runner enum-aware and resilient to empty outcomes\n\nThree independent fixes for the live dogfood runner, all in the\nmatrix-builder / acceptance-writer / summary-renderer codepath.\n\n1. Enum-aware happy_path values. `exampleValue` now prefers the first\n   declared enum value when a param carries spec-level enum constraints,\n   so `pet find-by-status --status available` runs instead of the\n   API-rejected `--status example-value`. PII-synthetic shapes still\n   win to keep browser-sniff captures from leaking customer data into\n   examples.\n\n2. `--write-acceptance` writes on every outcome. The flag previously\n   only emitted `phase5-acceptance.json` on PASS, forcing operators to\n   hand-author the FAIL marker the Phase 5.6 gate also forbids editing.\n   The runner now writes `status: \"pass\"` or `status: \"fail\"`\n   accordingly; the FAIL marker carries a `failure_summary` block\n   bucketing failures by category (transport_error, http_4xx, http_5xx,\n   exit_nonzero, output_mismatch, other) plus the contributing\n   commands. `phase5_gate.go` already routed `status: \"fail\"` to the\n   hold path, so the gate behavior is unchanged.\n\n3. Path Validity N/A for empty matrix. `printDogfoodReport` rendered\n   `0/0 valid (FAIL)` when SpecPath was present, the check was not\n   Skipped, and Tested was 0 — cosmetic divide-by-zero misalignment\n   with the scorecard, which reports 10/10 in the same run. Empty\n   matrices now render `N/A`, matching the failure aggregator at\n   `FailedIssues` that already guards on `Tested > 0`.\n\nThe phase5-acceptance JSON Schema gains an optional `failure_summary`\ndefinition, drops the unconditional `tests_passed >= 1` floor, and adds\na conditional rule that keeps the floor for `status: \"pass\"` markers.\n\nCloses #1384\n\n* fix(cli): tighten live-dogfood failure classifier per Greptile review\n\nAddress both findings on PR #1656 review of classifyLiveDogfoodFailure:\n\n1. Match \"invalid json\" / \"not json\" independently of the \"output\"\n   substring so the runner's own literal \"invalid JSON\" reason strings\n   bucket as output_mismatch rather than falling through to exit_nonzero.\n\n2. Check \"http 4\" before \"http 5\" so a retry-count log mentioning\n   \"http 5\" earlier cannot shadow a real 4xx response (e.g.,\n   \"retried http 5 times, status http 404\").\n\nAdds TestClassifyLiveDogfoodFailure covering both regressions plus the\ntransport_error, output+mismatch conjunction, exit_nonzero, and other\nbranches so future drift trips a targeted assertion instead of the\nexisting aggregate bucket-sum check in TestRunLiveDogfoodWritesFailMarkerOnFail.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"5cb39a69","date":"2026-05-19 08:30:33 -0700","author":"Trevin Chow","subject":"fix(skills): stop phase 4.95 code review from being skipped (#1655)","body":"* fix(skills): stop phase 4.95 code review from being skipped\n\nPhase 4.95's instruction told Claude Code to invoke `/review` for an\nin-place code review. But `/review` is PR-shaped — it fetches an open\nGitHub PR and comments back via `gh`. At Phase 4.95 there is no PR\nyet, so the agent hit the wrong-shape mismatch, then rationalized\nskipping the entire phase under the harness-exemption clause (\"no\nbuilt-in code review\"). Code review was silently dropping out of the\npipeline on Claude Code.\n\nRewrite the section goal-shaped (intent: local review *before* PR\nopen; CI-time review is the wrong fix window). Tool selection becomes\na survey-and-pick step with `compound-engineering:ce-code-review` and\n`/codex:review` as candidate skills and direct reviewer-subagent\ndispatch via the Agent tool named as the universal fallback. Explicit\nanti-pattern callout on `/review`. Narrow the harness exemption: skip\nis only legitimate when neither a review skill nor subagent capability\nexists, and \"first tool name didn't fit\", \"no PR yet\", and \"PR-time CI\nwill catch it\" are explicitly non-acceptable rationales.\n\nAlso slim the findings artifact. Fixed-in-place items collapse to a\none-line summary + commit hashes — the commits are already the record.\nFull enumeration is reserved for the three non-fix buckets\n(template-shape retro candidates, out-of-scope retro candidates,\nsurface-to-user findings) plus convergence outcome and review-path\nchosen.\n\n* fix(skills): clarify phase 4.95 round semantics and trim redundancy\n\nResolves Greptile P2 findings on #1655:\n\n1. Direct-subagent-dispatch path was ambiguous about what a \"round\"\n   means when 3-5 reviewers run in parallel. An agent could fix\n   correctness findings, re-run only that subagent, see it clear,\n   and declare convergence with security/maintainability findings\n   still open. Add: a round means re-running ALL spawned reviewers\n   in parallel, findings merge into a single set before autofix,\n   convergence is the merged set being empty.\n\n2. The Autofix policy block restated \"cheapest fix window\" verbatim\n   from the opening paragraph. Drop the second occurrence.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"2c3271fc","date":"2026-05-19 03:03:36 -0700","author":"Trevin Chow","subject":"fix(cli): detect path params in browser-sniff URL grouper (#1657)","body":"* fix(cli): detect path params in browser-sniff URL grouper\n\nHAR-sniffed specs collapsed variable path segments into literal endpoint\nnames because the normalizer only recognized UUID v4, lowercase hex\nhashes, and pure-numeric segments. Application IDs that ship a short\ntype prefix (`t_xxx`, `cus_xxx`), long opaque base62 / ULID / nanoid\ntails, or colon-composite discriminators (`create-image:reference:gpt-\nimage-2`) all landed in the spec as literal paths, producing per-ID\ncommand files that worked for the exact rows captured and nothing else.\n\nThis change:\n\n- Extends `normalizeEntryPath` with prefixed-ID, long-opaque-alnum, and\n  colon-composite heuristics, each guarded by a non-trivial-token or\n  mixed-case-or-digit floor so route literals like `subscriptions` or\n  `host:80` don't get parametrized.\n- Derives the placeholder name from the parent path segment\n  (`tables/t_xxx` -> `tables/{table_id}`) using a conservative\n  snake-case + trailing-`s` singularizer, with `{id}`/`{uuid}`/`{hash}`\n  as the fallback when the parent isn't usable as an identifier root.\n- Adds a `collapseVariantGroups` post-pass to `DeduplicateEndpoints`\n  and `DeduplicateTrafficEndpoints` that merges groups whose paths\n  differ in exactly one segment when at least one member's value\n  matches a strong ID shape, so cross-entry variance backstops shape\n  heuristics that miss.\n\nTests cover the OpenArt history-ID, Clay prefixed-ID, OpenArt colon-\ncomposite form-ID, and the issue's positive/negative acceptance cases.\n\nCloses #1386\n\n* fix(cli): address Greptile findings on browser-sniff path-param detection\n\nTwo issues flagged in PR #1657 review:\n\n1. (P1) `collapseVariantGroups` was unreachable as written. Its\n   `anyOpaque` gate called `looksLikeIDShape`, which uses the exact same\n   six regex patterns as `normalizeEntryPath` — so any segment that\n   would satisfy the gate had already been replaced by a placeholder in\n   the per-segment pass, putting it in a different bucket from the\n   literal-shaped peer it was supposed to merge with. Replace the gate\n   with `looksParameterizable`, a weaker check that's TRUE when a\n   segment has a digit, mixed case, or any strong-ID shape. Also\n   require ALL members at the varying position to satisfy this gate\n   (not just one), so a real literal like `health` paired with a\n   data-shaped sibling stays separate.\n\n   Adds a test covering the case Greptile flagged:\n   `/api/messages/abc123` and `/api/messages/xyz456` (6-char opaque\n   IDs, no prefix) now collapse to `/api/messages/{message_id}`.\n\n2. (P2) When two ID segments sit directly under the same parent\n   (`/resources/123/456`), the second one walked back past the\n   freshly-emitted `{resource_id}` placeholder, found `resources`\n   again, and emitted a second `{resource_id}` — producing a path with\n   duplicate parameter names that OpenAPI rejects. Track per-path\n   placeholder name use and append a counter suffix on collision, so\n   the path normalizes to `/resources/{resource_id}/{resource_id_2}`.\n\n   Adds dedicated single-entry and two-entry tests for the consecutive-\n   ID case.\n\n* fix(cli): address Greptile follow-up findings on variance pass\n\nTwo new P1 findings on the previous fix:\n\n1. `collapseVariantGroups` could still emit duplicate placeholder names\n   when a per-segment-placed `{resource_id}` lived earlier in the same\n   path. The variance pass walked back through the existing placeholder,\n   found `resources` again, and emitted a second `{resource_id}`. Build\n   a per-target use-count from the existing path's placeholders and\n   route the variance emission through `disambiguatePlaceholder`, so the\n   collision becomes `{resource_id_2}` just like the per-segment path.\n\n2. `collapseVariantGroups` was host-blind. `DeduplicateTrafficEndpoints`\n   keys its dedup map on host but `EndpointGroup` carried no host\n   field, so the variance pass bucketed entries from different hosts\n   together and could merge unrelated services into one group. Add a\n   `Host` field to `EndpointGroup`, populate it in\n   `DeduplicateTrafficEndpoints`, and include it in the variance\n   skeleton key.\n\nAdds tests for both: a two-entry case where per-segment normalization\nplants a placeholder before the variance position; and a multi-host\ncase asserting that two distinct hosts stay in separate groups even\nwhen their path shapes coincide.\n\n* fix(cli): narrow variance-pass gate to digit-only widening\n\nP1 follow-up: a pure mixed-case branch (hasUpper && hasLower) in\nlooksParameterizable was matching PascalCase route literals like\n/api/CreateDocument and /api/ListDocuments, which are common in\naction-style REST routes (ASP.NET, gRPC-HTTP transcoding). Two such\ndistinct endpoints would land in the same skeleton bucket and merge into\n/api/{api_id}, destroying both routes.\n\nDrop the mixed-case branch entirely. The has-digit branch alone is the\ncorrect widening for the variance pass's target — short opaque IDs like\nabc123/xyz456 all carry a digit. The rare digit-free opaque ID is left\nfor users to parametrize manually rather than risk false-positive\ncollapses on PascalCase action names.\n\nThe variance-pass disambiguation test now exercises digit-bearing\nshort-opaque IDs (abc123/xyz456 under /resources/) so it stays correct\nunder the narrowed gate. Adds an explicit negative case: PascalCase\nroute literals must stay separate.\n\n* fix(cli): make disambiguator scan past suffixed placeholders\n\nP1 follow-up: when a path already carries multiple suffixed\nplaceholders (`/resources/{resource_id}/{resource_id_2}/...`) and the\nvariance pass tries to emit a colliding `{resource_id}`, the previous\ndisambiguator pre-populated `used` with full placeholder strings and\nreturned `{resource_id_2}` — a name already in the path. Result:\n`/resources/{resource_id}/{resource_id_2}/{resource_id_2}`, invalid\nOpenAPI.\n\nRework `disambiguatePlaceholder` to loop on candidate names: start at\nthe base name, then `_2`, `_3`, ... until a name not already in `used`\nis found. Marks every emitted name in `used` so chains stay unique\nregardless of how the variance pass pre-populates the map.\n\nAdds a triple-consecutive test asserting that\n`/resources/123/456/abc123` and `/resources/789/012/xyz456` normalize\nto `/resources/{resource_id}/{resource_id_2}/{resource_id_3}`.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"17cd2774","date":"2026-05-19 11:14:15 +0200","author":"Mark van de Ven","subject":"fix(cli): dogfood prefers bundled <dir>/spec.yaml over caller --spec (#1625)","body":"* fix(cli): dogfood prefers bundled <dir>/spec.yaml over caller --spec\n\nWhen the caller invokes `printing-press dogfood --dir X --spec Y` and X\ncontains a spec archived by `publish package`, the archived spec is now\nauthoritative — caller's --spec is overridden. Fixes false-negative\nPath Validity / Auth Protocol regressions on multi-spec manuscripts\nruns (browser-sniff, crowd-sniff, hand-authored-on-top-of-official),\nwhere the caller picks the upstream spec instead of the CLI's own.\n\nFalls through to caller's --spec unchanged when no bundled spec exists.\n\nFixes #1620\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>\n\n* fix(cli): typed DogfoodSpecSource const, drop ticket refs\n\nAddress Greptile review:\n- Introduce DogfoodSpecSource type + DogfoodSpecSourceBundled /\n  DogfoodSpecSourceCaller consts (AGENTS.md: categorical strings ->\n  typed const at introduction). Matches MCPSurfaceState pattern.\n- Remove ticket-number references from code comments (AGENTS.md: no\n  ticket numbers in code comments).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>\n\n* test(golden): refresh dogfood fixtures for bundled-spec auto-discovery\n\nTwo intentional behavior changes from the dogfood spec-resolution fix:\n\n- dogfood-verdict-matrix: stderr gains the `dogfood: using spec ...\n  (bundled)` line; fail-path-auth-dead.json gains spec_source field.\n- generate-golden-api: same stderr + JSON additions. Also, auth_check\n  and browser_session_check now run substantively (they were skipped\n  previously because the test invokes `dogfood --dir X` without\n  --spec; with bundled-spec auto-discovery the checks engage against\n  <X>/spec.yaml as intended).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>\n\n* fix(cli): clarify auth_check.detail when bundled spec has no recognized scheme\n\nNow that dogfood resolves a bundled spec instead of short-circuiting on\nno-spec, checkAuth's \"expectedPrefix empty\" branch is reachable with a\nspec present. The old message \"spec not provided or no bot/bearer/basic\nscheme detected\" contradicted spec_source=\"bundled\" + a non-empty\nspec_path in the report. Drop the \"spec not provided\" half — that case\nno longer reaches this branch.\n\n---------\n\nCo-authored-by: Mark van de Ven <10142972+markvandeven@users.noreply.github.com>\nCo-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>\nCo-authored-by: Trevin Chow <trevin@trevinchow.com>"},{"hash":"ac3e15b4","date":"2026-05-19 01:58:44 -0700","author":"salmonumbrella","subject":"fix(cli): sync pagination, auth aliases, narrative timing, and codex prompt (#1341)","body":"* fix: sync pagination, auth aliases, narrative timing, and codex prompt\n\nSix independent generator-level findings surfaced during a downstream CLI\ngeneration run against an offset-paginated REST API. Each is generic; the\ndiscovery context is not in the diff or test data.\n\n1. AuthHeader OR-semantics for legacy x-auth-env-vars (spec.go)\n\n   IsAuthEnvVarORCase only returned true when all EnvVarSpecs were\n   non-required per_call. Specs using the legacy x-auth-env-vars list\n   form (or a populated EnvVars slice with default Required=true) fell\n   through to the canonical-only path: only the first env-var alias\n   reached AuthHeader, the rest were populated by Load() but never\n   read. Updated IsAuthEnvVarORCase to also return true when:\n     * EnvVarSpecs has 2+ entries that are all request-credentials, OR\n     * legacy EnvVars list has 2+ entries.\n   New pin: TestAuthHeader_LegacyEnvVarsList_OrSemantics.\n\n2. validate-narrative side-effect classifier (narrativecheck.go)\n\n   --full-examples ran every command whose --help advertised --dry-run,\n   including auth set-token YOUR_TOKEN_HERE and auth logout. The first\n   persisted the literal placeholder to the user's ~/.config; the\n   second wiped saved credentials. Added isSideEffectfulNarrativeExample\n   skip-classifier covering auth set-token, auth logout, auth setup,\n   auth login, --launch, and bare --apply. Skipped commands return\n   StatusUnsupported with an explanatory reason rather than executing.\n   New pins: TestRunFullExample_SkipsAuthSetToken,\n   TestRunFullExample_SkipsAuthLogout.\n\n3. Empty-array marshalling guidance (codex-delegation.md)\n\n   Codex-generated novel-feature commands declared var results []T,\n   which marshals nil to JSON null. Empty list outputs broke jq '.[]'\n   agent pipelines. Updated the transcendence-command prompt template\n   to require results := make([]T, 0) for all list-shape outputs, with\n   a paragraph at the top of CONVENTIONS spelling out the contract.\n\n4. Shipcheck leg ordering (shipcheck.go)\n\n   dogfood ran first and synthesized README.md and SKILL.md from\n   research.json.novel_features_built — including any planned commands\n   whose command-paths didn't actually resolve against the built binary.\n   validate-narrative ran later and caught them but the user-facing\n   files were already wrong. Reordered: verify, validate-narrative,\n   dogfood, workflow-verify, verify-skill, scorecard. Comment updated\n   to reflect the dependency: verify builds the binary; validate-narrative\n   checks research.json against the binary BEFORE dogfood renders from it.\n\n5. Offset+has_more pagination loop (sync.go.tmpl)\n\n   The sync loop's break-condition required nextCursor != \"\" alongside\n   hasMore=true. APIs using offset+has_more (no cursor token in the\n   response) returned hasMore=true with empty nextCursor on every page,\n   so the loop broke after page 1 and only the first page worth of\n   records reached the local store. Fixed at both occurrences (linear\n   and parent-child sync paths): when cursorParam==\"offset\" and the\n   envelope returned no cursor despite hasMore, compute nextOffset\n   client-side as currentOffset + pageSize.limit. Cursor-based APIs\n   that genuinely return no cursor still break (existing behavior;\n   silent infinite-loop guard). New pin:\n   TestGeneratedSyncAdvancesOffsetWhenHasMoreWithoutCursor.\n\n6. Per-resource pagination param gate (profiler.go + sync.go.tmpl)\n\n   Sync sent ?limit= and ?offset= to every list endpoint regardless of\n   whether the spec declared those params for that endpoint. List\n   endpoints that don't paginate (typical for /me, /settings, small\n   reference resources) rejected the synthetic params with HTTP 400\n   \"Invalid query parameter\". Added profiler.SupportsPagination per\n   syncable+dependent resource (detected from endpoint params at\n   generate time). Generator template emits resourceSupportsPagination()\n   from the per-resource map; sync gates limit/offset setting on its\n   return value. New pin:\n   TestGeneratedSyncGatesPaginationParamsPerResource.\n\nTest coverage:\n  * go build ./... — pass\n  * go vet ./... — pass\n  * go test ./... — pass (30 packages, ~13min runtime)\n\n* fix: use exact side-effect flag matching\n\n* fix: detect cursor-only pagination support\n\n* test(cli): refresh sync pagination goldens\n\n---------\n\nCo-authored-by: Trevin Chow <trevin@trevinchow.com>\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"275646a3","date":"2026-05-19 03:33:06 -0500","author":"Nick Walker","subject":"feat(cli): add auth set-token escape hatch for cookie-auth CLIs (#1641)","body":"* feat(cli): add auth set-token escape hatch for cookie-auth CLIs\n\nThe auth_browser template now emits an `auth set-token <jwt>` subcommand\nalongside login/status/logout. This is the escape hatch for sites whose\naccess token cannot be reached by the cookie-jar extractor in\n`auth login --chrome` — most commonly Auth0 SPA SDK v2+ deployments that\nkeep the JWT in JS heap memory and never expose it to cookies or\nlocalStorage. Users (or agents) paste the bearer from DevTools instead of\nhand-editing config.toml.\n\nValidation runs through cliutil.LooksLikeJWT (shipped in PR #1602 / WU-2),\nso short Cloudflare-shaped tracking cookies like `__cf_bm` get rejected\nwith a hint pointing back at the DevTools paste flow. Emission is gated\non Auth.Type != \"none\", so auth.type=none + persisted-query CLIs (the\nonly path that routes a no-credentials spec through this template) don't\nship a subcommand they have nothing to save into.\n\nTwo stale assertions in the cookie/composed auth_browser regression\ntests used the literal \"set-token\" as their proxy for \"this is not the\nauth_simple template.\" That proxy no longer holds — auth_browser now\nlegitimately emits set-token too. Swapped to `newAuthSetupCmd` as the\nauth_simple-only signature, since auth_browser uses newAuthLoginCmd for\nthe cookie/chrome flow instead.\n\nRefs #1598 (WU-3a).\n\nCo-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n\n* fix(cli): zero TokenExpiry in auth set-token to avoid stale write\n\nSaveTokens used to receive cfg.TokenExpiry from the loaded config, which\nin the escape-hatch scenario is a past timestamp from an expired prior\nsession. The auth_browser status command doesn't consult TokenExpiry\ntoday, so the bug wasn't immediately surfacing — but writing a known-\nstale value violates the zero-on-write invariant that ClearTokens and\nSaveBearerToken already follow in config.go.tmpl, and would mislead any\nfuture expiry-aware status check.\n\nPassing time.Time{} keeps the API \"we don't know the lifetime\"; the real\nexpiry surfaces via 401 on the next call, same as the rest of the\nbrowser-auth flow.\n\nRefs #1598 (WU-3a).\n\nCo-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n\n---------\n\nCo-authored-by: Claude Opus 4.7 <noreply@anthropic.com>\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"f9bb82c8","date":"2026-05-19 00:17:49 -0700","author":"Trevin Chow","subject":"fix(skills): reprint discovers and carries prior patches (#1649)","body":"* fix(skills): reprint discovers and carries prior patches\n\nReprinting silently dropped post-publish hand-fixes recorded in\n.printing-press-patches.json, regressing live-validated API quirks\nand architectural patterns. Phase B now re-fetches the public\npatches file (covering the case where amends landed without a\nregen, so local can lag even when run_id matches), and Phase D\nthreads non-empty patches into the /printing-press hand-off under\na new \"## Prior Patches\" heading.\n\nPatches are framed as an informational watch-list, not a re-apply\nmandate — the machine may have absorbed some upstream, and the\nretro+bugfix loop is the right mechanism for that drift, not a\nper-patch reconciliation gate.\n\n* fix(skills): make patches discovery's fetch durable\n\nAddress Greptile P2 findings: the prior mktemp-based fetch leaked the\ntemp file on the success path (accumulating across repeated reprint\nruns) and used the ephemeral path as the downstream `$PATCHES_SOURCE`\nreference, which a long-running /printing-press hand-off could find\nabsent.\n\nRefresh the local patches file from public via mktemp + atomic\nrename instead. The temp file is always either renamed or removed,\nand PATCHES_SOURCE is always the stable local path the downstream\nagent can open."},{"hash":"b5b1dd59","date":"2026-05-18 19:26:15 -0500","author":"Nick Walker","subject":"feat(cli): add cliutil.LooksLikeJWT shared validator with length floor (#1602)","body":"* docs(cli): file factor75 retro — 3 systemic findings from live dogfooding\n\nLive debugging surfaced three machine-side gaps that compound on any\nsniffed BFF CLI:\n\n1. Hand-authored sync silently drops URL personalization params,\n   degrading responses to a generic preselect without changing JSON shape.\n2. looksLikeJWT has no minimum-length floor and saves short Cloudflare /\n   tracking cookies as access tokens.\n3. auth login --chrome can't reach Auth0 SPA SDK in-memory tokens (now\n   the SDK default since v2 deprecated localStorage as XSS-unsafe).\n\nTracked as GitHub issue #1598 with three WUs that can be picked up\nseparately. The Factor75 printed CLI's matching syncer fix is in the\nlocal library (~/printing-press/library/factor75/) and is unaffected by\nthis commit.\n\nCo-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n\n* feat(cli): add cliutil.LooksLikeJWT shared validator with length floor\n\nAdds a generator-emitted cliutil helper that every printed CLI now\ncarries: LooksLikeJWT(string) bool and FindJWTInCookieJar(jar) string,\nboth with a 150-char total / 20-char header minimum that filters out\nCloudflare bot-management cookies, CSRF tokens, and other short shapes\nthat share JWT's three-base64url-segments structure.\n\nUntil now, sniffed CLIs that need to validate a JWT shape (e.g. when\nextracting a Bearer from a cookie jar) had to hand-author the check,\nand the hand-authored versions inherit a false-positive: any three-\nsegment base64url string passes regardless of length. The factor75\nprinted CLI hit this with the Cloudflare `__cf_bm`-shaped value\n`01KRPVRYA2SNQT9BAGD6984WAG_.tt.1` (31 chars), which got saved as the\naccess token and produced confusing HTTP 401 \"invalid character\"\ndecode errors on every subsequent API call.\n\nThe helpers are purely additive — no current template calls them, so\nno rendered output changes for existing CLIs. They become useful as\nsoon as auth templates start needing them (next PR: WU-3a `auth\nset-token` subcommand uses LooksLikeJWT as its validator).\n\nRefs #1598 (WU-2).\n\nCo-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n\n* fix(cli): address PR review feedback on jwtshape helper (#1602)\n\n- Tighten boundary tests to hit 149/150 exactly so constant changes\n  (150 -> 155) or off-by-one comparison drift surface immediately.\n  Previous fixtures sat at 108 and 158 chars, neither exercising\n  the floor.\n- FindJWTInCookieJar now strips a leading \"Bearer \" from the cookie\n  value before returning, matching LooksLikeJWT's input normalization.\n  A cookie carrying the Authorization wire value (\"Bearer eyJ...\")\n  used to return the prefixed form, which would produce a double-\n  prefixed Authorization header when a caller built the header from\n  the result. New test case proves the bare token comes back.\n\nCo-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n\n---------\n\nCo-authored-by: Claude Opus 4.7 <noreply@anthropic.com>\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"ed17d302","date":"2026-05-18 17:13:34 -0700","author":"Trevin Chow","subject":"fix(cli): gate ship plans on agent readiness (#1638)","body":"* fix(cli): gate ship plans on agent readiness\n\n* docs(cli): document readiness ship gate learning\n\n* fix(cli): clean readiness finding rendering\n\n* fix(cli): normalize readiness table findings\n\n* fix(cli): clarify readiness verdict holds\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"7eee7dfc","date":"2026-05-18 16:46:18 -0700","author":"Trevin Chow","subject":"fix(cli): harden browser-sniff PII placeholders (#1639)","body":""},{"hash":"8c955701","date":"2026-05-18 16:08:24 -0700","author":"Trevin Chow","subject":"fix(cli): correct live-check and helper emission (#1637)","body":""},{"hash":"af9ba8c7","date":"2026-05-18 13:20:26 -0700","author":"Trevin Chow","subject":"docs(skills): document separate-file pattern for extending emitted files (#1624)","body":"* docs(skills): document separate-file pattern for extending emitted files\n\nAdd a Phase 3 callout to skills/printing-press/SKILL.md warning that\nhand-edits to any generator-emitted file (`config.go`, `client.go`,\n`auth.go`, `store.go`, `root.go`, plus the `cliutil_*` / MCP / sync\nfamilies) are wiped on `printing-press generate --force` and reconciled\nby `printing-press regen-merge`. Only whole hand-authored files survive\nacross regen; inline additions (struct field, header in do(),\nAddCommand call, migration row) are not preserved.\n\nNames the separate-file pattern per common extension case:\n\n- Custom config fields -> internal/config/<api>_config.go\n- Custom request headers -> internal/client/<api>_headers.go\n- Custom auth flow -> internal/auth/<api>_auth.go\n- Extended store schema -> internal/store/<api>_migrations.go\n- New novel command -> internal/cli/<feature>.go\n\nWhen no separate-file route exists (an AddCommand call with no registry\nhook, a case branch in a method switch), the callout directs agents to\nfile a generator issue rather than inline-editing.\n\nCloses #1604\n\n* docs(skills): reconcile novel-command bullet with Phase 3 skeleton; scope header injection\n\nTwo Greptile findings on PR #1624:\n\nP1 - \"New novel command\" bullet contradicted the existing Phase 3\nskeleton (lines 2491-2495), which instructs agents to wire hand-authored\ncommands via AddCommand in root.go. The earlier text implied the body\nfile alone was sufficient, and the closing paragraph routed AddCommand\nedits to \"file a generator issue.\" An agent following that guidance\nwould ship a command file that is never registered in the Cobra tree.\n\nReworded to:\n- The command body lives in internal/cli/<feature>.go (survives regen).\n- The AddCommand call still goes in root.go per the existing skeleton.\n- `generate --force` wipes that call, but `regen-merge` re-injects it via\n  the lost-registration mechanism in internal/pipeline/regenmerge/apply.go.\n- Prefer regen-merge over --force for refreshes.\n- Spec-declared commands need no hand-wired AddCommand at all.\n\nP2 - \"Custom request headers\" bullet said the exported func is called\n\"before each request,\" implying global injection. The generated client.go\nhas no per-request mutator chain; the only injection surface is\nGetWithHeaders / PostWithHeaders called explicitly by novel code.\n\nReworded to make the scope explicit: the helper builds the header map,\nnovel code passes it to GetWithHeaders/PostWithHeaders, and generated\nendpoint commands are unaffected.\n\nAlso tightened the closing fallback to exclude AddCommand (which is\nalready covered by regen-merge) and call out which inline diffs actually\nwarrant a generator-issue ask.\n\n* fix(skills): correct auth extension path and remove fictional method switch\n\nThe custom-auth-flow extension pattern pointed agents at\n`internal/auth/<api>_auth.go`, but no `internal/auth/` package exists —\nall four auth templates (`auth.go.tmpl`, `auth_simple.go.tmpl`,\n`auth_browser.go.tmpl`, `auth_client_credentials.go.tmpl`) emit to\n`internal/cli/` under `package cli`. Following the guidance produced\nan orphan package that nothing in the generated tree imports.\n\nAlso dropped the reference to a \"templated method switch in `auth.go`\":\nthe generated `auth.go` uses constructor functions\n(`newAuthLoginCmd`, `newAuthSetupCmd`, etc.), not a switch.\n\nAligns the auth extension with the side-by-side same-package invariant\nthat the other four extension patterns already follow\n(`internal/config/`, `internal/client/`, `internal/store/`,\n`internal/cli/`).\n\n* Update skills/printing-press/SKILL.md\n\nCo-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>\nCo-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>"},{"hash":"766354e6","date":"2026-05-18 13:13:18 -0700","author":"Trevin Chow","subject":"feat(cli): add sync --path-context KEY=VAL runtime override for template placeholders (#1631)","body":"* feat(cli): add sync --path-context KEY=VAL runtime override for template placeholders\n\nThe narrower spec-extension fix #1368 makes per-tenant sync work when the\nspec declares `info.x-tenant-env-var` and the user's environment holds\nthe value. That covers the steady-state ServiceTitan case but leaves\nthree gaps named on the issue:\n\n- One-off override at the call site (env says A, this run should target B).\n- Placeholders the spec did NOT annotate with an env var, so the\n  existing `EndpointTemplateVars` substitution has nothing to read.\n- The workspace/org generalization beyond `{tenant}`.\n\nThe `--path-context KEY=VAL` flag (repeatable) on `sync` plugs all three\ninto the same generated machinery `EndpointTemplateVars` already uses.\nAt RunE time the values land in `c.Config.TemplateVars`, so the existing\n`buildURL` substitution pipeline picks them up for every request without\nnew request-path manipulation.\n\nProfiler-driven emission: gated on `.EndpointTemplateVars` being non-\nempty so plain APIs (which have nothing to substitute) keep their\nexisting sync flag surface intact. The matching byte-compat test asserts\nthe flag is absent from plain CLIs.\n\nCloses #1332\n\n* fix(cli): warn on unknown --path-context keys; drop assertions belonging to #1632\n\nGreptile flagged two findings on PR #1631:\n\n1. Four test assertions referenced template output (emitCacheRefreshFailedEvent,\n   cache_warning, refresh_failed) that does not exist on this branch. Those\n   symbols come from PR #1632's auto_refresh template change and were\n   accidentally included here. Removing them so the freshness-helper test\n   passes on this branch; #1632 carries the matching assertions.\n\n2. --path-context silently accepted keys outside endpointTemplateVarSet,\n   so typos like --path-context tneant=... would store the value in\n   Config.TemplateVars without substituting anywhere, leaving the run\n   looking correct but using the env-resolved default. Emit a one-line\n   stderr warning per unknown key. Non-fatal so callers passing\n   forward-compat keys aren't blocked.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"187b6992","date":"2026-05-18 13:00:42 -0700","author":"Trevin Chow","subject":"fix(cli): emit structured cache_warning JSON on auto-refresh failure (#1632)","body":"* fix(cli): emit structured cache_warning JSON on auto-refresh failure\n\nWhen autoRefreshIfStale's bounded refresh hits an upstream error and the\ncommand proceeds with the stale cache, the generator now emits a\none-line JSON event to stderr alongside the existing prose warning:\n\n  {\"event\":\"cache_warning\",\"reason\":\"refresh_failed\",\"resources\":[\"homes\"],\"error\":\"...\"}\n\nmeta.freshness already carries this signal in --json output for commands\nthat wrap with wrapWithProvenance, but novel commands that build their\nown response shape (and most agent-facing fan-outs to printed CLIs) only\nsaw the prose warning on stderr. Agents reading the stdout stream had no\nparseable way to know subsequent rows were served from a cache that\ndoesn't reflect their requested filters.\n\nThe new emitter sits next to the prose warning so both stay aligned; the\nprose stays for humans, the JSON line gives agents a single grep-able\nsignal that matches the established sync_warning/sync_error vocabulary.\nThe matching auto_refresh_test.go.tmpl exercises the emitter shape in\nevery generated CLI with cache enabled.\n\nCloses #1263\n\n* fix(cli): panic-safe captureStderr + generator assert for auto_refresh_test.go\n\nAddresses Greptile findings on #1632:\n- P1: captureStderr lacked deferred cleanup, so a panic in fn() would leak\n  the io.ReadAll goroutine and hang the test binary. Add a defer that\n  closes the write end of the pipe and restores os.Stderr, leaving the\n  happy-path explicit calls as the primary path.\n- P2: TestGenerateFreshnessHelperEmitted now asserts auto_refresh_test.go\n  is emitted with the expected helpers and snippets. Golden fixtures have\n  no cache-enabled case, so without this a Go syntax error in the\n  template would only surface when a customer runs go build.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"e798f671","date":"2026-05-18 12:40:50 -0700","author":"Trevin Chow","subject":"fix(cli): route Swagger 2.0 specs through openapi2conv to avoid OOM on circular refs (#1630)","body":"* fix(cli): route Swagger 2.0 specs through openapi2conv to avoid OOM on circular refs\n\nSwagger 2.0 specs with circular $ref chains in definitions/ (Tripletex,\nNetSuite REST, Salesforce Tooling) caused the generator to burn 15-30\nminutes of CPU at 1.8-4.4 GB RSS and eventually OOM. The same content\nconverted to OpenAPI 3.x externally via swagger2openapi generated in\nabout three minutes.\n\nDetect Swagger 2.0 at the parser boundary (normalized JSON has top-level\n\"swagger\": \"2.0\") and route through openapi2conv.ToV3WithLoader before\nthe OpenAPI 3 loader runs. The conversion rewrites #/definitions/X to\n#/components/schemas/X so the existing cycle-aware OpenAPI 3 code path\nhandles resolution. openapi2conv is a sub-package of kin-openapi, which\nis already a dependency.\n\nEmit one stderr line announcing the conversion so operators have\nvisibility into the multi-minute resolution phase. The retro called out\nthe silent phase as the biggest UX issue in the failure mode.\n\nCloses #1241\n\n* fix(cli): address Greptile feedback on Swagger 2.0 loader (P1 + 2 P2s)\n\nP1 (Missing ReadFromURIFunc): the conversion path's loader did not\ninstall the custom ReadFromURIFunc that the main OpenAPI 3 path uses,\nso a Swagger 2.0 spec loaded with a file location whose external $refs\npointed at YAML companion files would skip the YAML->JSON normalization\nstep and fail deep inside ToV3WithLoader. Extract the loader setup into\nnewConfiguredOpenAPI3Loader and call it from both paths.\n\nP2 (Overly permissive version match): drop the \"swagger\":\"2\" prefix\nvariants; the Swagger 2.0 spec mandates exactly \"2.0\" so the bare\n\"2\" forms only matched hypothetical future version strings. Add\ntest fixtures asserting \"2\" and \"2.5\" do not trigger detection.\n\nP2 (Goroutine leak on timeout): document the intentional leak in the\n30s-timeout branch of the cyclic-ref regression test. Parse exposes no\ncancellation hook; failing the test fast and abandoning the goroutine\nis the lesser evil compared to letting CI sit for ~25 minutes before\nOOMing.\n\nRefs #1241\n\n* fix(cli): detect Swagger 2.0 via streaming JSON decode, not head substring\n\nGreptile P1: the substring-scan detector silently missed real-world\nSwagger 2.0 specs. normalizeSpecData round-trips through encoding/json,\nwhich sorts map keys alphabetically — so \"swagger\" (s) lands AFTER\n\"definitions\" (d) and \"paths\" (p) in the serialized output. For a\nmulti-MB vendor spec (Tripletex, NetSuite, Salesforce), the \"swagger\"\nmarker is far past any reasonable head-buffer window, and the detector\nreturned false on the exact specs this PR was built to handle.\n\nSwitch to a streaming json.Decoder that walks top-level keys, returning\ntrue only when a top-level \"swagger\" key has the value \"2.0\". Stops\nreading after the swagger value is found; for non-Swagger 2.0 specs it\ntraverses tokens at the top level (skipping nested values en bloc),\nwhich is still fast for the ~2MB upper bound of real specs.\n\nAdd a regression test that builds a Swagger 2.0 fixture whose alphabetized\nserialization pushes the \"swagger\" marker well past the 4KB mark.\n\nRefs #1241\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"042a78d4","date":"2026-05-18 12:24:41 -0700","author":"Trevin Chow","subject":"feat(cli): flag empty defaultSyncResources at dogfood time (#1633)","body":"* feat(cli): flag empty defaultSyncResources at dogfood time\n\nWhen a spec exposes no bulk-list endpoint (only parameterized detail pages and\nrequired-query searches, e.g. Allrecipes), the profiler correctly produces an\nempty SyncableResources slice, but the generator still emits `sync` as a\nruntime no-op alongside store-dependent novel commands (cookbook, pantry,\ntop-rated, ...). The CLI ships with no advertised path to populate the store.\n\nAdd a dogfood pipeline_check signal that detects the empty-list emission of\ndefaultSyncResources() in the generated sync.go and surfaces it as WARN so the\ngap is visible at shipcheck time. Also emit a one-line runtime hint (stderr\nin human mode, sync_warning JSON event in agent mode) when the structural\nprecondition holds, so callers see \"no bulk-list endpoints in spec; populate\nthe store via single-fetch commands\" instead of a silent total_records:0.\n\nCloses #1156\n\n* fix(cli): always emit sync_file_emitted in pipeline_check JSON\n\nGreptile flagged a JSON-tag asymmetry on PipelineResult: SyncFileEmitted\ncarried `omitempty` but SyncResourcesPresent did not, so when sync.go is\nabsent a downstream consumer sees `sync_resources_present: false` with no\nsync_file_emitted signal, which incorrectly reads as \"should have resources\nbut does not.\" Drop the `omitempty` so both fields always appear together\nand consumers can disambiguate the three real states:\n\n  sync_file_emitted=false, sync_resources_present=false  no sync surface\n  sync_file_emitted=true,  sync_resources_present=true   healthy\n  sync_file_emitted=true,  sync_resources_present=false  empty (issue #1156)"},{"hash":"14bc18a4","date":"2026-05-18 10:20:09 -0700","author":"Trevin Chow","subject":"feat(cli): default mcp.transport=[stdio, http] for small APIs (#1629)","body":"* feat(cli): default mcp.transport=[stdio, http] for small APIs\n\nSpecs that leave the mcp.transport field unset now get http compiled in\nalongside stdio when the typed-endpoint surface is at or under\nspec.DefaultRemoteTransportEndpointThreshold (30). Stdio-only servers\ncan only reach clients that can spawn a subprocess; cloud-hosted agents\nneed a remote transport. Adding http at small surface sizes costs\nnothing in tool count or agent context and lifts mcp_remote_transport\nfrom 5/10 to 10/10 in the scorecard.\n\nExplicit transport lists pass through unchanged: a spec that opts into\nmcp.transport: [stdio] stays stdio-only even at small endpoint counts.\nLarger APIs continue to fall back to stdio-only by default, where the\ndominant problem is tool-count, not reach, and warnUnenrichedLargeMCPSurface\nkeeps recommending the orchestration pattern.\n\nResolution lives on a new APISpec.EffectiveMCPTransports helper so the\ntemplate branch reads from a single source of truth that knows both the\nconfigured field and the endpoint count. MCPConfig.EffectiveTransports\nremains the unconditioned view for spec validation and mcp_audit.\n\nCloses #1603\n\n* docs(cli): clarify Transport field comment per Greptile review\n\nThe inline struct-tag comment on MCPConfig.Transport said \"empty == [stdio]\",\nbut after the small-API default the empty case resolves via\nAPISpec.EffectiveMCPTransports to [stdio, http] for small APIs and to\n[stdio] only for larger ones. Update the comment to reflect that."},{"hash":"f671b7ba","date":"2026-05-18 01:26:36 -0700","author":"Trevin Chow","subject":"feat(ci): mirror supply-chain hardening from printing-press-library (#1619)","body":"* feat(ci): mirror supply-chain hardening from printing-press-library\n\nAdds a layered PR-time gate against the workflow-trust and Go-module env\nattack shapes observed in May 2026 (TanStack mini-Shai-Hulud,\nBufferZoneCorp, node-ipc).\n\nTwo layers, applied to PRs touching .github/workflows/**:\n\n1. Deterministic Python scan in .github/scripts/verify-supply-chain/,\n   vendored from mvanhorn/printing-press-library (source dated 2026-05-17).\n   scan.py is verbatim; signals.py is adapted to the generator-repo signal\n   set — R3 (replace directives in library/**/go.mod), R5 (npm lifecycle),\n   and R6 (module-path drift) are omitted because they protect surfaces\n   that don't exist here. R1 (pull_request_target + PR-head checkout),\n   R2 (id-token outside allowlist; empty allowlist here), and R4\n   (GOPROXY/GOFLAGS/GONOSUMCHECK overrides) apply with minor adaptation.\n   15 unit + integration tests pass.\n\n2. Four Greptile rules covering the same signals plus a judgment-only\n   credential-path read rule for internal/**/*.go and cmd/**/*.go.\n\nCompanion PR in mvanhorn/printing-press-library:\nhttps://github.com/mvanhorn/printing-press-library/pull/665\n\nCanonical incident timeline and primary-source citations live in the\npublished-library repo's docs/solutions/security/2026-05-supply-chain-hardening.md\n— single source of truth across both repos.\n\nInformational on landing — promote to required check after a one-week\ngreen window confirms no false-positive miscalibration on real PRs.\n\n* fix(ci): close scanner self-bypass gap in supply-chain workflow\n\nMirrors the workflow fix from mvanhorn/printing-press-library#665.\n\nOriginal posture: workflow checked out PR head, so the scan.py\nexecuting the gate was whatever the PR shipped. A malicious PR\ncould weaken scan.py and bypass detection of its own payload.\n\nNew posture: check out the base branch from the base repo (the\ntrusted scanner version runs), fetch the PR head SHA as a\ndetached ref, and pass it to scan.py via --head-ref.\n\nForward-looking concern — this check is informational on\nlanding but matters once promoted to a required gate.\n\n(R3 / block-form replace evasion does not apply here — the\ngenerator-repo mirror omits R3 entirely; see signals.py header.)\n\n* fix(ci): bootstrap fallback when base has no scanner yet\n\nMirrors the printing-press-library fix. The previous workflow rewrite\nruns scan.py from the base branch, but this PR is what introduces\nscan.py, so base has nothing to run. Adds an if-not-exists fallback\nthat restores .github/scripts/verify-supply-chain/ from the PR head\nSHA. No-op after merge.\n\n* fix(ci): R1 flow-sequence trigger + diff-aware R1/R2/R4\n\nMirrors the printing-press-library fix. Two gaps Greptile flagged\non PR #665 of the published-library repo apply equally here:\n\n1. R1 trigger regex missed YAML flow-sequence form\n   `on: [pull_request_target, push]`. Added a second regex\n   (_PR_TARGET_TRIGGER_FLOW) and unified via _has_pr_target_trigger().\n\n2. R1/R2/R4 scanned full head_content rather than diff additions.\n   Refactored via a shared _lines_to_scan() helper: full scan for\n   new files, added_lines only for existing files. Prevents false\n   positives if main ever drifts to contain a pattern these rules\n   cover.\n\n19 tests pass (was 15).\n\n* chore(ci): mirror scan.py signature change from printing-press-library\n\nPR #665 added rename-aware diff handling to scan.py: changed_files()\nreturns old_path for renames/copies, and build_change() fetches\nbase_content from old_path on renames. The signature change is benign\nin the generator-repo mirror (R3/R5/R6 are omitted here, so no signal\ncurrently consumes the rename information), but kept in lock-step\nwith the source to make future cherry-picks of signal changes\nmechanical.\n\nMirror commit only. Tests unchanged (19 pass).\n\n* fix(ci): deletion-guard + id-token regex edge case (mirror)\n\nMirrors the printing-press-library fix:\n\n1. Workflow gains a deletion-guard step that hard-fails any PR which\n   removes files under .github/scripts/verify-supply-chain/. Closes the\n   staged delete-then-replace attack against the bootstrap fallback.\n\n2. _ID_TOKEN_WRITE regex now accepts an optional trailing YAML comment\n   (`id-token: write  # justification`) so attackers can't evade the\n   strict end-of-line match.\n\n20 tests pass (was 19).\n\n* docs(agents): trim supply-chain section to operational guidance\n\nMirror of the printing-press-library trim. AGENTS.md no longer narrates\nthe incident set or enumerates signals; greptile.json is authoritative\nfor rule coverage, and the published-library solutions doc carries the\nincident timeline + primary sources.\n\n* fix(ci): YAML-parse R1/R2/R4 to close block-scalar bypass\n\nGreptile flagged on PR 1619 (4/5): a workflow using YAML block-scalar\nnotation evades the single-line regex.\n\n  ref: >-\n    \\${{ github.event.pull_request.head.sha }}\n\nBoth the regex `_DANGEROUS_REF` and `_ID_TOKEN_WRITE` required the\nsensitive value on the key's line; valid YAML with the value folded\nonto the next line silently passes. Each new YAML quirk (block-form\nreplace, flow-sequence trigger, now block-scalar values) needed its\nown regex patch — that's a brittle approach.\n\nSwitched R1/R2/R4 to parse the workflow with pyyaml (pre-installed on\nubuntu-latest runners) and walk the parsed dict tree:\n\n- R1 walks jobs → steps → uses=actions/checkout* → with.ref, and\n  matches the loaded ref value against a (still-regex) pattern for\n  dangerous expressions. YAML normalises block-scalars before the\n  match runs, so all forms collapse to the same string.\n- R2 walks workflow-level and job-level `permissions.id-token`. Also\n  catches `permissions: write-all` which grants id-token implicitly.\n- R4 walks env blocks at workflow, job, and step level.\n\nDiff-awareness is now STRUCTURAL: parse both base and head, fire only\nif head has the dangerous pattern AND base didn't. Replaces the\nline-level _lines_to_scan approach for these signals — more robust\nbecause reformatting (reindent, change YAML style) of an unchanged\ndangerous pattern no longer false-fires.\n\nTwo new tests cover the folded (`>-`) and literal (`|-`) block-scalar\nref forms. The refs/pull regex also fixed to use [^\\\\n] instead of\n[^\\\\s] so spaces inside \\${{ ... }} don't break the match.\n\n22 tests pass (was 19).\n\n* fix(ci): R1 catches github.head_ref + merge_commit_sha shortcuts\n\nGreptile P1: the dangerous-ref pattern missed github.head_ref (shorthand\nalias for event.pull_request.head.ref) and event.pull_request.merge_commit_sha\n(GitHub's synthesised test-merge commit). Both resolve to PR-author-controlled\ncontent under pull_request_target — same elevated-context attack surface as\nthe previously-covered forms.\n\nSame fix mirrored in mvanhorn/printing-press-library#667.\n\n24 tests pass (was 22).\n\n* chore(ci): gitignore __pycache__ and untrack stray .pyc files\n\nverify-supply-chain Python tests were generating .pyc files that got committed because .gitignore had no Python rules.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"1dfbf795","date":"2026-05-17 22:57:05 -0700","author":"Matt Van Horn","subject":"chore(main): release 4.9.0 (#1495)","body":""},{"hash":"ef81ca04","date":"2026-05-17 22:29:57 -0700","author":"Trevin Chow","subject":"test(cli): update TestPublishSkillSkipsCliSkillsMirrorRegen for new library gate name (#1618)","body":"#1614 (docs(skills): publish skill aligns with library's bot-only\ngenerated-artifacts rule) updated the publish skill's prose to\nreference the new `Fail on changes to generated artifacts` gate that\nmvanhorn/printing-press-library#659 introduced. The existing contract\ntest still asserted the old strings:\n\n- \"Guard against hand-edits to cli-skills mirror\" (gate name retired)\n- \"Do NOT regenerate or commit \\`cli-skills/pp-<api-slug>/SKILL.md\\`\n  here\" (now extended to cover both cli-skills and registry.json)\n\n#1614's CI passed because change-scoped test selection didn't include\nthe pipeline shard (the skill change touched skills/, not internal/\npipeline/). The full-suite matrix that runs on release-please PRs\ncaught the divergence on the next release prep (release-please#1495\nfailing on test (pipeline)).\n\nUpdated the assertions to match the current skill text. Intent is\nunchanged: verify the skill names the current rejection mechanism and\nexplicitly warns against committing the generated files."},{"hash":"dde2a4e0","date":"2026-05-17 21:01:41 -0700","author":"Trevin Chow","subject":"docs(skills): publish skill aligns with library's bot-only generated-artifacts rule (#1614)","body":"mvanhorn/printing-press-library#659 replaces the older Guard +\nauto-fix + fork-only drift trio with a single check, `Fail on changes\nto generated artifacts`, that hard-fails any PR — fork or same-repo —\nwhose diff touches `registry.json` or `cli-skills/pp-*/SKILL.md`. The\nin-PR auto-fix path is gone; the only legitimate flow is to drop those\nfiles from the diff and let post-merge regen (`generate-skills.yml`,\n`generate-registry.yml`) produce them.\n\nThree references in the publish skill described the old behavior:\n\n1. The intro paragraph after the trigger phrases (line ~30) cited\n   `Guard against hand-edits to cli-skills mirror`.\n2. The Step 6 bash comment block (line ~461) described the\n   fork-only-pre-rejection + same-repo-auto-fix split.\n3. The Greptile-feedback section (line ~942) cited \"your edits will\n   be overwritten\" without naming the gate.\n\nEach now references `Fail on changes to generated artifacts` and the\nsingle uniform behavior.\n\nNo behavior change in the skill itself — Step 6 already does not\nregenerate or commit these files. This is doc alignment only."},{"hash":"fef027fe","date":"2026-05-17 12:16:04 -0700","author":"Trevin Chow","subject":"fix(skills): retro skill scrubs secrets and PII before posting issue bodies (#1595)","body":"* fix(skills): retro skill scrubs secrets and PII before posting issue bodies\n\nThe retro skill already scrubs artifact zips (via references/secret-scrubbing.md\nLayers 1-4) before catbox upload, but issue body text went straight from the\nretro doc to `gh issue create` with no scrub step. A finding *about* a\nsecret-leak could quote the actual leaked value as \"evidence\" and re-leak it\nin a public GitHub issue. The retro doc itself was also unscrubbed despite\nbeing preserved in manuscript proofs and read by future runs' dedup scan.\n\nThis change closes both gaps:\n\n1. **New cardinal rule** at the top of the retro skill's Cardinal rules\n   section: issue bodies and retro docs are public surfaces and must be\n   scrubbed before posting / writing. Specifically warns against the\n   \"quote the leaked value as evidence\" failure mode that retro findings\n   about secret leaks are most vulnerable to.\n\n2. **New Layer 0 in `references/secret-scrubbing.md`**: a reusable\n   `scrub_body` shell function that operates on a single markdown file\n   (the retro doc or an issue body). Hard-fails on unredacted vendor-prefix\n   tokens (Stripe, GitHub, Slack, AWS, OpenRouter, Anthropic, Linear,\n   Mailchimp, JWT, Bearer-with-value); auto-redacts PII (real emails,\n   NANP phones, ZIP+4, Mailchimp inbox-ids) in place. Allowlists RFC 2606\n   reserved example domains and NANP fictional 555-01XX phone numbers\n   so legitimate documentation examples pass through unchanged.\n\n3. **Phase 5 (retro doc write) scrubs the doc** immediately after writing\n   it, before the manuscript proofs + scratch copies are made canonical.\n   Hard-fails surface as a stop with explicit redaction instructions; PII\n   auto-redacts and writes the cleaned version in place.\n\n4. **Phase 6 Step 3 (issue/comment post) scrubs body files** before each\n   `gh issue create` / `gh issue comment` call. Per-WU hard-fails route\n   into the existing `$FAILED_ISSUES` reporting as a new `scrub-failed`\n   outcome kind so the user sees what needs hand-redaction.\n\n5. **Body template updated** to explicitly warn agents to redact in the\n   \"What we observed\" section of new issue bodies; references the Layer 0\n   shape so the agent has the redaction format at hand.\n\n6. **New skill-level rule** added to the Rules section: never quote a\n   leaked secret as evidence of a secret-leak finding.\n\nVerified end-to-end with smoke tests against four input shapes:\n- clean input (no findings) passes through with zero diff\n- Linear API key (`lin_api_<40+ chars>`) hard-fails with line numbers\n- Mailchimp API key (`{32-hex}-us6`) hard-fails with line numbers\n- PII (real email, NANP phone, ZIP+4, Mailchimp inbox-id) auto-redacts;\n  RFC 2606 example.com and NANP 555-01XX pass through unchanged\n\n`go test ./internal/cli/...` green; `scripts/golden.sh verify` green.\n\nScope: skill markdown only, no Go code changes. No new binary subcommand\nneeded in this change; the scrubbing is bash-resident in the skill so\nusers running the skill outside a printing-press source checkout still\nget the protection.\n\n* fix(skills): retro scrub_body — fictional 555-01XX exclusion handles paren format (Greptile P2 on #1595)\n\nThe original NANP phone regex anchored the (?!555[-\\s.]?01) negative lookahead\nbefore the optional opening paren \\(? — so when a fictional number is written\nas `(555) 012-3456`, the position before the `(` is not where `555` lives,\nthe lookahead never fires, and the fictional number gets auto-redacted as\nPII. API doc examples in that style would be mangled in scrubbed output.\n\nReplace the negative lookahead with a capture-and-decide-inline approach:\nextract the area code ($1) and exchange ($2), then check `$a eq \"555\" &&\n$e =~ /^01/` inside a /e replacement.\n\nTwo Perl gotchas the new form has to handle:\n\n1. **`$1`/`$&` aren't reliably accessible after the regex engine hands the\n   replacement to the callback.** Snapshot into lexicals immediately.\n2. **The inner `$e =~ /^01/` test resets `$&` to \"01\".** Without snapshotting\n   `$&` before the inner match, the carve-out path returns just \"01\" instead\n   of the whole original phone string.\n\nThe fix snapshots `$&`, `$1`, `$2` into `$w`, `$a`, `$e` *before* running\nthe inner regex, then uses `$w` (lexical copy of the whole match) for the\npreserved path.\n\nVerified against an expanded test matrix:\n\n  REAL phones (redact):\n    425-761-6499, (425) 761-6499, +1 425-761-6499, +1 (425) 761-6499,\n    4257616499, 425.761.6499 — all → <REDACTED:phone-us>\n\n  FICTIONAL 555-01XX (pass through):\n    555-0142, 555-0100, (555) 012-3456, (555) 0123456, +1 555-012-3456,\n    +1 (555) 012-3456, 5550123456, 555.012.3456 — all preserved\n\n  OTHER PII (redact): emails, ZIP+4, Mailchimp inbox-id — all redact\n  ALLOWLIST (pass through): @example.com, @example.net, @example.invalid\n  HARD-FAIL (refuse to write): lin_api_*, {32-hex}-us\\d{1,2} — all hard-fail\n\nComment + code update explain the Perl variable-lifetime trap so future\nmaintainers don't accidentally re-introduce it.\n\n* fix(skills): preserve scrub-failed body files; reconcile inline-bodies principle with --body-file (Greptile P2 on #1595)\n\nThe retro skill's first scrub-body pass had two follow-up gaps Greptile flagged\non the re-review of d4d4c27f:\n\n1. **The cleanup destroyed the recovery path it advertised.** The\n   `scrub-failed` failure message tells the agent the body file is \"left at\n   $BODY_TMP for hand-redaction\" — but the unconditional `rm -rf\n   \"$ISSUE_TMPDIR\"` at the end of the loop wiped the file before the agent\n   could read it. The recovery message was a lie.\n\n   Fix: cleanup is now conditional on the scrub-failed count. If any WUs\n   failed scrub, the temp dir survives so the body file is hand-redactable;\n   the loop emits a NOTE to stderr telling the user where the surviving\n   files are. The dir lives in `mktemp -d` so it self-cleans on the next\n   `tmpwatch` / OS reboot regardless. Clean runs (no scrub-failed) wipe the\n   dir as before.\n\n2. **The \"Execution principles\" block forbade `--body-file`** because in\n   the original design body construction via the `Write` tool added a\n   per-WU tool round-trip — the single largest source of perceived\n   latency. The new scrub_body step writes to a bash-resident temp file\n   inside one `Bash` invocation, scrubs, then passes the scrubbed file to\n   `gh --body-file`. There are no extra tool round-trips; the principle's\n   *intent* (avoid Write-tool calls per issue) is still honored, but the\n   wording (\"never pass --body-file\") read as forbidding the new approach.\n\n   Fix: update the principle to specifically allow bash-resident temp files\n   while still forbidding per-WU Write-tool round-trips. Adds a one-line\n   explanation that `--body-file` is the natural consumer of the scrub\n   step's file output.\n\nBoth fixes flow from Greptile's framing: \"the hard-fail protection works,\nbut the recovery path it advertises is broken\" and \"the stale execution\nprinciple contradicts the new --body-file approach.\" Confidence on the new\nshape should land at 4-5/5 after the re-review."},{"hash":"c3e1b87a","date":"2026-05-17 10:30:22 -0700","author":"Trevin Chow","subject":"feat(cli): emit .printing-press-patches.json on every generate (#1590)","body":""},{"hash":"caa68801","date":"2026-05-17 10:21:12 -0700","author":"Trevin Chow","subject":"fix(cli): drop // PATCH marker instruction from generated AGENTS.md","body":"The patches index manifest is the single recorded artifact for library-side\ncustomizations. Pairs with the library-side relaxation at\nmvanhorn/printing-press-library#644 which drops the verifier check\nrequiring bidirectional pairing between .printing-press-patches.json\nentries and // PATCH source comments.\n\nThe template's \"Local Customizations\" section now describes a single step\n(catalog the change in the patches manifest) rather than two (mark every\nsource site AND catalog). A closing note clarifies that inline // PATCH\ncomments are optional navigation aids if an agent wants them, but the CI\nno longer requires them.\n\nGolden fixtures updated for generate-golden-api and\ngenerate-golden-api-rich-auth (the two cases whose expected AGENTS.md\ninclude the customizations section); all 20 golden cases pass verify.\n\nExisting printed CLIs keep their old AGENTS.md until they regen. No\nretrofit needed because the library verifier (post-#644) doesn't check\nthe source markers either way."},{"hash":"875d64e9","date":"2026-05-17 01:54:35 -0400","author":"tallyberner","subject":"fix(cli): detect partial-failure response shape in :mutate handlers, add --allow-partial-failure flag (#1360)","body":"* fix(cli): detect partial-failure response shape in mutate handlers\n\nAdd --allow-partial-failure flag. Mutate envelope's success now depends\non both HTTP status and absence of body-level partial-failure (e.g.\nGoogle Ads partialFailureError). Generic detector lives in helpers.go,\nnot coupled to one API: any 2xx body with a top-level\npartialFailureError carrying a non-zero code or non-empty message is\nflagged, results[].resourceName is extracted, and the typed exit\n(code 6, distinct from apiErr's code 5) fires unless\n--allow-partial-failure downgrades it to a stderr warning.\n\nDetection runs once per mutate call before output-mode selection so\ntable, JSON envelope, --quiet, and raw paths all surface the same\nexit code. partialFailureReport is emitted into the envelope under\npartial_failure with field/message/code/details/resource_names so\nagents can route per-operation remediation.\n\nRuntime test in generator_test.go drops a *_test.go into a generated\nPetstore CLI module and runs go test against the emitted\ndetectPartialFailure; this is the load-bearing safeguard that proves\nthe detector works on a Google-Ads-shaped body, not just that the\nstrings are emitted.\n\n* fix(cli): enforce partial-failure exit on mutate raw/quiet output path\n\nCodex review flagged a gap: when neither the wantsHumanTable nor the\nasJSON/piped branch handled a mutate response, control fell through to\nprintOutputWithFlags without checking partialFailure. That meant\n--quiet, --csv, --plain, and default terminal raw output would exit 0\non a detected partial failure even though the warning was printed to\nstderr — same silent-swallow regression the patch is supposed to be\npreventing for JSON consumers.\n\nWrap the fall-through call so the typed code-6 exit fires unless\n--allow-partial-failure is set, matching the table and JSON paths.\n\n* test(cli): update golden fixtures for partial-failure detection\n\nUpdated 18 golden cases to reflect the new emitted code:\n  - helpers.go gains detectPartialFailure + partialFailureErr +\n    partialFailureReport.\n  - root.go gains the --allow-partial-failure persistent flag and the\n    rootFlags.allowPartialFailure field.\n  - mutate command handlers (POST/PUT/PATCH/DELETE) gain the detection\n    block at the top of the response handling, partialFailure-aware\n    envelope serialization, and the fall-through partial-failure exit\n    guard so --quiet/--csv/--plain/raw output paths surface code 6 too.\n\nBehaviour change is intentional and matches the patch in the prior two\ncommits.\n\n* fix(cli): include allowPartialFailure in JSON envelope success\n\nWhen --allow-partial-failure is set and a partial failure is detected, the\nprocess exits 0 (the user opted in to allow it) but the JSON envelope was\nstill emitting success: false. Automated callers parsing the envelope to\ndecide whether to retry or alert would see a failure signal on an\nintentionally-allowed partial result, disagreeing with the exit code.\n\nAlign the envelope's success expression with the exit-code semantics:\ntreat the request as successful when the status is 2xx AND there is no\npartial failure OR the caller passed --allow-partial-failure. Regenerated\ngoldens mirror the template fix in every affected case.\n\n---------\n\nCo-authored-by: dior <agents@futureremodeling.com>\nCo-authored-by: Trevin Chow <trevin@trevinchow.com>"},{"hash":"80ba507e","date":"2026-05-16 16:25:33 -0700","author":"Trevin Chow","subject":"fix(ci): cancel-in-progress=false so cancelled check_runs don't pile up","body":"Mirrors printing-press-library#625. With cancel-in-progress: true,\nmulti-event bursts (synchronize + review.submitted + multiple\nreview_comment.created within seconds) leave cancelled auto check_runs\nbehind that we can't PATCH away post-Feb 2025. Switch to\ncancel-in-progress: false so queued runs execute serially and all\ncomplete with terminal success/failure conclusions."},{"hash":"833213ff","date":"2026-05-16 16:25:04 -0700","author":"Trevin Chow","subject":"fix(cli): drive browser-sniff http_transport from HAR HTTP-version distribution (#1540)","body":"* fix(cli): drive browser-sniff http_transport from HAR HTTP-version distribution\n\nPre-fix the browser-sniff parser wrote spec.http_transport = browser-chrome-h3\nwhenever reachability.mode was browser_clearance_http and the bare\nbrowser-chrome enum whenever mode was browser_http, regardless of the HTTP\nversion the HAR actually recorded. Origins that serve H/2 but not H/3 (most\nCloudflare and Next.js + Vercel SaaS today) shipped CLIs that failed their\nfirst live call with CRYPTO_ERROR 0x128 (remote): tls: handshake failure.\n\nAdds a new browser-chrome-h2 enum + UsesBrowserHTTP2Transport predicate.\nCaptures HAR httpVersion on every entry, computes a per-target distribution\nin TrafficAnalysisSummary, and maps the majority version to the matching\ntransport: H/3 -> browser-chrome-h3, H/2 -> browser-chrome-h2, H/1.1 ->\nbrowser-chrome (no version force). Empty distribution falls back to bare\nbrowser-chrome so the runtime negotiates rather than guesses.\n\nThe bare browser-chrome enum now means \"no version force\"; the implicit\nHTTP/2 force from the pre-fix template requires opt-in via browser-chrome-h2.\nEffectiveHTTPTransport's default-sniffed and browser-auth-for-HTML branches\nreturn browser-chrome-h2 instead of bare browser-chrome so the implicit H/2\nforce is preserved for shipped CLIs that did not go through browser-sniff.\n\nApplyReachabilityDefaults now runs before applyHTTPTransportDefault so the\nHAR-driven mapping wins for browser_http / browser_clearance_http modes.\nbrowser_required intentionally leaves HTTPTransport empty since the operator\nmust drive a real browser.\n\nCloses #1387\n\n* fix(cli): allow --transport browser-chrome-h2 via the CLI flag\n\nnormalizeHTTPTransport's CLI-flag allow-list was the one place that did\nnot learn about the new browser-chrome-h2 enum. Operators passing\n--transport browser-chrome-h2 hit the validator's reject path even\nthough the value is accepted by spec.Validate, catalog.Validate, and the\ngenerator's enum predicates. Add it to the allow-list and extend the\nerror message and the existing test to cover the new value.\n\nRefs #1387\n\n* fix(cli): preserve implicit H/2 force on protection-driven transport path\n\napplyHTTPTransportDefault writes spec.HTTPTransportBrowserChrome (bare,\nno force after the template change) whenever trafficAnalysisRecommendsBrowserTransport\nfires for non-reachability-mode signals: Cloudflare/DataDome/Akamai/PerimeterX\nprotections, html_scrape protocol, generic browser/scrape hints. Pre-PR the\ntemplate's else branch unconditionally emitted ForceHTTP2() for bare\nbrowser-chrome; post-PR it emits neither, so specs flagged via those\nheuristics lost their implicit H/2 force.\n\nSwitch this fallback to browser-chrome-h2 so the generated client still\nforces HTTP/2 in the absence of HAR HTTP-version data. The reachability-\ndriven paths (browser_http / browser_clearance_http) keep using the HAR\ndistribution via ApplyReachabilityDefaults; explicit H/3 hints still win\nover the protection default.\n\nRefs #1387\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"63f745b8","date":"2026-05-16 16:22:48 -0700","author":"Trevin Chow","subject":"fix(skills): split SQL example out of go-fenced block in NULL-safe scans guidance","body":"Per Greptile review on PR #1530: the COALESCE example was appended\ninside the same ```go fenced block as the Go snippets, so syntax\nhighlighters, AI tools that read SKILL.md, and lint passes that\nvalidate fenced blocks would all interpret the bare SELECT as Go.\nMove the SQL into its own ```sql fenced block with a short\nintroductory sentence, leaving the Go snippets self-contained."},{"hash":"f7365c26","date":"2026-05-16 16:09:25 -0700","author":"Trevin Chow","subject":"fix(ci): close jq if-then-else with end so >10-thread path doesn't crash","body":"Mirrors printing-press-library#623: outer if in the listing jq filter\nwas missing 'end'. jq emits a parse error and exits non-zero; set -e\nbails the shell before the step summary and ::error:: annotation can\nwrite, so authors see a cryptic jq error instead of the thread list."},{"hash":"94332417","date":"2026-05-16 16:07:02 -0700","author":"Trevin Chow","subject":"fix(ci): use job exit-code instead of POST/PATCH for check_run state","body":"Mirrors printing-press-library#623 redesign. POST/PATCH approach\nhits HTTP 403 in CI as of Feb 2025: GitHub Actions runtime rejects\nworkflow-side modification of check_run status/conclusion. Switch\nto relying on the auto-created check_run for the job (its conclusion\nmirrors the job's exit status) and exit 1 when threads are unresolved.\n\nNet: simpler workflow, no API calls, no duplicates, behavior matches\nthe post-Feb-2025 GitHub Actions runtime."},{"hash":"c96ac5da","date":"2026-05-16 15:53:25 -0700","author":"Trevin Chow","subject":"fix(cli): writeThroughCache caches single-object responses keyed by non-id PKs (#1531)","body":"* fix(cli): writeThroughCache caches single-object responses keyed by non-id PKs\n\nwriteThroughCache guarded its single-object cache write with\n`if _, ok := envelope[\"id\"]; ok` — APIs whose primary key is named\nCertNo / sku / invoiceId / etc. silently fell through every lookup.\nThe user saw HTTP 200 and correct stdout while every downstream\nfeature that reads from the local SQLite store (smart presets,\nholdings analysis, offline search) got no data.\n\nDrop the hardcoded \"id\" guard and delegate primary-key resolution\nto UpsertBatch's existing resourceIDFieldOverrides path, which the\nparser already populates from `x-resource-id` and the response-schema\ninference chain.\n\nTightened the guard to skip list-shaped envelopes whose array\nhappened to be empty — `{\"items\": []}` must not write a row keyed\nby the whole envelope. Caught by TestClientBasePathLiveRequest on\nthe first pass; explicit regression added.\n\nCloses #1439\n\n* fix(cli): verify list-wrapper field is an array before skipping cache write\n\nGreptile flagged that the list-envelope guard checked for KEY presence\nonly — any detail object whose own field happened to be named data,\nresults, or items (with a scalar/object/null value) would be treated\nas an empty list envelope and silently dropped. That regressed the\nprior envelope[\"id\"] path, which would have cached the row.\n\nTightened the guard to verify the value at the wrapper key actually\nunmarshals as a JSON array. A scalar-valued field with a colliding\nname is a regular response field, not a list envelope, and the row\nstill goes through UpsertBatch.\n\nAdded TestWriteThroughCacheCachesObjectWithListWrapperFieldName as\nexplicit coverage for the collision case Greptile called out.\n\n* fix(cli): distinguish JSON null from empty array on list-wrapper field\n\nGreptile noted that json.Unmarshal(\"null\", &arr) succeeds with arr\nleft nil, so the previous unmarshal-only check would misclassify\n{\"CertNo\":\"x\",\"items\":null} as an empty list envelope and silently\ndrop the row. Require arr != nil so true empty arrays stay in the\nskip branch while JSON null falls through to UpsertBatch as a\nregular field collision.\n\nAdded TestWriteThroughCacheCachesObjectWithNullWrapperFieldName as\nthe missing coverage Greptile called out.\n\n* chore(cli): empty commit to force CI rerun after flaky text file busy\n\n* fix(cli): require list envelope to be fully metadata-shaped before skipping\n\nGreptile flagged round-3: a detail object whose own field happened to\nbe named items/data/results AND holds an empty array (e.g.\n{\"id\":\"order_123\",\"items\":[],\"status\":\"pending\"}) was being treated\nas an empty list envelope and silently dropped. The previous \"any\nwrapper key with array value\" heuristic was too permissive.\n\nSwitch to a structural check: an envelope is list-shaped only when\nEVERY top-level key is either the wrapper itself (results/data/items)\nor one of a known pagination-metadata key (next_cursor, has_more,\ntotal, links, meta, response_metadata, etc.). A single non-metadata\nkey signals real per-row data and the row gets cached.\n\nCodifies the metadata allowlist as a package-level var so it stays\nauditable. Adds explicit regression tests for the multi-key detail\nshape and for the real list-envelope-with-metadata shape.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"5b8a8141","date":"2026-05-16 15:53:08 -0700","author":"Trevin Chow","subject":"fix(ci): scope settle delay to synchronize, strip head_sha from PATCH","body":"Mirrors Greptile feedback addressed on printing-press-library#623:\n\nP1: head_sha is valid in POST /check-runs but is not a valid body\nparameter for PATCH /check-runs/<id>. If GitHub strictly validates\nand 422s the unknown field, the PATCH would fail and set -e bails.\nStrip head_sha for the PATCH branch via jq del().\n\nP2: settle delay fired for every pull_request_target action. Only\nsynchronize moves the commit pointer and triggers GitHub's async\nthread auto-resolution, so only synchronize can race it. Gate the\ndelay on action=synchronize to avoid an unnecessary 8s wait on PR\nopens and draft transitions."},{"hash":"162d9a32","date":"2026-05-16 15:47:45 -0700","author":"Trevin Chow","subject":"fix(ci): conversation-resolution check race + duplicate check_run","body":"Three flaws in the workflow shipped with this PR that printing-press-library\n#587 surfaced (then closed in mvanhorn/printing-press-library#623). Backing\nthe same fixes into the cli-printing-press copy before this merges:\n\n1. Race condition. synchronize fires the moment a push lands, but GitHub\nmarks review threads outdated/resolved asynchronously based on the new\nfile content. Querying immediately races that resolution and can report\nstale 'unresolved' for a thread that is about to flip resolved a second\nlater. Add 8s settle delay on pull_request_target events specifically.\n\n2. Job name collision. The job was named the same as the user-facing\ncheck ('All conversations resolved'), so GitHub Actions auto-created a\ncheck_run with that name reflecting only the job's exit status (always\nsuccess). Our explicit POST created a SECOND check_run with the same\nname reflecting actual thread state. Both showed up on the SHA. Rename\nthe job to 'Evaluate review threads' so the auto-created check_run has\na distinct internal name and stays out of the merge widget.\n\n3. Duplicate check_runs across runs. Every workflow run POSTed a new\ncheck_run rather than updating, so a stale failure could sit next to a\nfresh success indefinitely. Switch POST to PATCH-if-exists keyed on\n(name=All conversations resolved, app=github-actions).\n\nAfter this lands the merge widget shows exactly one\n'All conversations resolved' entry per PR, with state always matching\nthe latest workflow run."},{"hash":"f55f57a8","date":"2026-05-16 14:41:23 -0700","author":"Trevin Chow","subject":"fix(ci): address Greptile review on conversation-resolution check","body":"Three findings from Greptile on this PR:\n\n1. (P1) Failure summary cited the Mergify condition by name in\n.mergify.yml, but #1536 has not merged so that condition does not\nexist on main yet. Replaced with a repo-convention message accurate\nboth today and after #1536 lands.\n\n2. (P2) The --arg PR_NUMBER binding was passed to jq but no longer\nreferenced in the listing function (I removed the use in the\ncli-printing-press port but left the binding). Dead arg, dropped.\n\n3. (P2) The draft-PR guard only checked pull_request_target events,\nso reviews and comments on draft PRs still fired the workflow.\nRewrote the guard to skip drafts across every PR event source while\nallowing workflow_dispatch through as an explicit manual ask."},{"hash":"21ae8a54","date":"2026-05-16 14:34:36 -0700","author":"Trevin Chow","subject":"feat(ci): surface unresolved review threads as a status check","body":"Ports the conversation-resolution-check workflow from the downstream\nlibrary repo (mvanhorn/printing-press-library#620, #621). The gate\nitself is enforced by Mergify (#1536 adds the\n`#review-threads-unresolved = 0` condition to .mergify.yml). This\nworkflow is the visibility layer.\n\nWhy both: Mergify's existing `Mergify Merge Protections` check_run\nis the most readable surface for \"what's missing\" after a maintainer\napplies `ready-to-merge`, but pre-label it just says \"skipping\" with\nno specific signal about thread state. An author who pushed a fix\nbut didn't click \"Resolve conversation\" sees green CI, green Greptile\nReview, and no obvious cue. This workflow puts an explicit\n\"All conversations resolved\" check_run in the status list regardless\nof label state.\n\nLifecycle coverage: pull_request_target for new commits,\npull_request_review for new reviews from Greptile, and\npull_request_review_comment for new inline findings.\nconcurrency.cancel-in-progress coalesces bursts. Workflow_dispatch\nprovides a manual escape hatch.\n\nNote: GitHub Actions does NOT support pull_request_review_thread as\na trigger event, so clicking \"Resolve conversation\" in the UI does\nnot auto-fire this workflow. The check refreshes on the next push,\nreview, or comment. This gap is documented inline in the workflow\nfile. (Library repo #621 was the hotfix that taught us this.)"},{"hash":"a8fa80a3","date":"2026-05-16 14:27:26 -0700","author":"Trevin Chow","subject":"fix(ci): require all review threads resolved before queue + merge","body":"Mergify's queue_conditions and success_conditions both gate on the\nGreptile Review check_run being success/neutral/skipped, but that\ncheck_run only reflects the confidence score — it does not track\nwhether Greptile's inline P0/P1 findings have actually been resolved.\nSo a PR with unresolved review threads can today:\n\n1. Pass all CI checks (build-and-test, generated-test, etc.)\n2. Pass Greptile Review (score >= threshold)\n3. Get labeled ready-to-merge\n4. Auto-merge via Mergify, with the findings still open\n\nPR #1514 is the canonical example today: mergeStateStatus=CLEAN with\none unresolved Greptile thread. The downstream library repo had the\nsame gap and closed it via a ruleset rule + a visibility check_run\n(mvanhorn/printing-press-library#620, #621). This repo uses Mergify\ninstead of rulesets, so the equivalent fix is a Mergify-native\ncondition.\n\nAdding `#review-threads-unresolved = 0` to both queue_conditions and\nsuccess_conditions means:\n\n- Mergify won't queue a PR with unresolved threads\n- Mergify won't merge a PR that picks up new findings during the\n  in-place queue re-run\n- Mergify's status comments will explicitly cite the unresolved\n  threads as the blocker, so authors see what to do\n\nRelease-please PRs are bot-generated and reliably have zero review\nthreads, so the condition passes them through transparently — no\nexemption needed for the existing release-please branch pattern."},{"hash":"4e304df5","date":"2026-05-16 13:37:13 -0700","author":"Trevin Chow","subject":"fix(cli): scope-aware sync --param dispatch with --global-param fallback (#1529)","body":"* fix(cli): scope-aware sync --param dispatch with --global-param fallback\n\nAsana, Jira, GitHub, and similar APIs reject path-scoped dependent\nrequests when a top-level scope param is re-injected on top of the path\ncontext (e.g. /projects/<gid>/tasks?workspace=<gid> trips Asana's\n\"Must specify exactly one of project, tag, ...\" error). The sync\ncommand's --param flag injected indiscriminately into every request,\nforcing a manual two-phase workaround.\n\nSplit syncUserParams.global into flatGlobal (--param) and trueGlobal\n(--global-param). applyTo gains an isDependent bool: dependent\npath-scoped calls skip flatGlobal but still apply trueGlobal and\nper-resource params. Existing single-phase syncs on APIs without\ndependent resources behave identically.\n\nOperators who relied on the old apply-everywhere semantic opt back in\nwith --global-param key=value.\n\nCloses #1393\n\n* refactor(cli): rename parseSyncUserParams perResourceFlags param\n\nMake the parameter name in the helper signature match the call-site name resourceParamFlags. No behavior change; just removes a name-mismatch readability snag inside the function body.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"7396d66f","date":"2026-05-16 13:35:41 -0700","author":"Trevin Chow","subject":"fix(skills): add NULL-safe SQL scan guidance to Phase 3 store-query starter","body":"Novel-feature commands authored against generated typed FTS/upsert\ntables or against json_extract on the resources blob silently drop\nevery row whose scanned column is NULL. `database/sql`'s `rows.Scan`\ninto a bare `string`/`int64`/`float64` returns a non-nil error on NULL\n(\"converting NULL to string is unsupported\"), and the conventional\n`for rows.Next()` loop continues past scan errors — so the query\nreturns zero records, no error reaches the caller, and the feature\nlooks healthy because the upstream API call succeeded.\n\nThe Phase 3 store-query RunE skeleton in skills/printing-press/SKILL.md\ndocuments the resources-table-keyed query pattern but says nothing\nabout NULL handling. Add an inline scan-comment in the skeleton and a\nparallel-shape \"NULL-safe SQL scans MUST use sql.Null* targets (or\nCOALESCE) for any column that can be NULL\" paragraph after the\nstreaming-frame normalizers callout, with right/wrong code snippets.\n\nThe generator does not emit per-resource typed query helpers\n(writeThroughCache stores opaque JSON via UpsertBatch; resolveLocal\nreads opaque JSON), so the issue's `comp:generator` framing was off —\nthe fix surface is the SKILL prompt that drives Phase 3 novel-feature\nauthoring.\n\nCloses #1438\nRefs #1469 (NULL-safe SQL sub-bullet addressed here; the\nhelper-enumeration sub-bullet remains open under that issue)"},{"hash":"c31632b3","date":"2026-05-16 13:14:54 -0700","author":"Matt Van Horn","subject":"feat(skills): add /printing-press-amend skill — dogfood + direct-input modes (#1490)","body":"* feat(cli): add verify-internal-skill subcommand\n\nLints internal-skill SKILL.md files (frontmatter + canonical sections).\nDistinct from verify-skill, which validates a printed CLI's SKILL.md\nagainst its Go source — internal skills (polish, retro, publish, amend)\nhave no internal/cli/ to verify against.\n\nChecks: frontmatter-parse, frontmatter-required (name, description,\nallowed-tools), name-matches-dir, allowed-tools-shape, body-has-heading\n(warn). All three existing internal skills pass cleanly.\n\n* feat(skills): scaffold printing-press-amend skill\n\nNew skill that wraps the dogfood-to-PR loop for a printed CLI in\nmvanhorn/printing-press-library. Mines the active session transcript\nfor friction, scopes with the user, plans + executes the fix, scrubs\nPII, opens a PR. Two user-in-loop checkpoints (scope, PR draft).\n\nThis commit lands the SKILL.md skeleton (frontmatter + setup contract +\nphase-header placeholders) plus the brainstorm requirements doc and\nimplementation plan that drove it. Phase bodies (U2-U7) land in\nfollow-up commits.\n\nAdds skills/printing-press-amend/SKILL.md to the\nTestSkillSetupBlocksMatchWorkspaceContract test slice — setup-contract\nparity with publish/score/catalog enforced.\n\n* fix(cli): use strings.SplitSeq in verify-internal-skill\n\nSatisfies golangci-lint modernize hint. Pure refactor — same behavior,\nno test changes needed.\n\n* feat(skills): flesh out printing-press-amend phase bodies + references\n\nAdds the substantive Phase 1-7 bodies to skills/printing-press-amend/\nSKILL.md, plus three reference files:\n\n- transcript-parsing.md (Phase 1: read active session transcript,\n  extract friction signals, classify bug-vs-feature, auto-detect\n  target CLI, confirm with user)\n- pii-scrubbing.md (Phase 5: three-layer scrub — credentials reused\n  from retro/secret-scrubbing.md, entities from a user-maintained\n  stop-list at ~/.printing-press/amend-config.yaml, plus first-mention\n  defense for unrecognized capitalized phrases)\n- library-pr-plumbing.md (Phase 6+7: managed-clone bootstrap, branch\n  collision detection, issue ownership, push + gh pr create with\n  durable HEAD_SHA evidence URLs — adapted from publish Steps 5/7/8)\n\nPhase 4 operates directly on the managed clone of mvanhorn/printing-\npress-library at $PRESS_HOME/.publish-repo-$PRESS_SCOPE (per the\nplan's pre-implementation decision), enforces the public library's\nmandatory `// PATCH(...)` source-comment + .printing-press-patches.json\ncontract, and runs `printing-press publish validate` with up-to-3\nretry iterations.\n\nTwo user-in-loop checkpoints: scope confirmation after capture (U4),\nPR draft review before any gh command fires (U7).\n\n* docs(cli): add /printing-press-amend to README skill catalog\n\nOne-line entry under Publish, in the same details/summary shape as the\nother skills.\n\n* feat(skills): expand printing-press-amend frontmatter for direct-input mode\n\nBroaden description to cover both dogfood and direct-input input modes.\nAdd trigger phrases for user-supplied asks (rename, add feeds, sniff for\nnew APIs, amend with these ideas).\n\nU1 of docs/plans/2026-05-16-001-feat-printing-press-amend-direct-input-mode-plan.md\n\n* feat(skills): add Phase 0 input mode detection to printing-press-amend\n\nInsert a new \"## Phase 0 — Input Mode Detection\" section between Setup\nand Phase 1. Documents the detection rubric (dogfood, direct, both,\nambiguous), the AskUserQuestion fallback, the default-dogfood preservation\nrule, and the runstate persistence path. Phase 1's existing dogfood body\nis untouched; U3 introduces the direct-input sub-section.\n\nU2 of docs/plans/2026-05-16-001-feat-printing-press-amend-direct-input-mode-plan.md\n\n* feat(skills): split printing-press-amend Phase 1 into mode-conditional sub-sections\n\nRename \"## Phase 1 — Friction Capture\" to \"## Phase 1 — Capture\" and\nsplit into ### 1a (dogfood, existing body lifted verbatim) and ### 1b\n(direct-input, new). Document the shared typed finding list shape with\nnew \"provenance\" field. Reserve ### 1b.i for the sniff subroutine\n(filled in by U4).\n\ntranscript-parsing.md gets a scope note pointing to direct-input-parsing.md\nfor the new mode; its body is unchanged.\n\nU3 of docs/plans/2026-05-16-001-feat-printing-press-amend-direct-input-mode-plan.md\n\n* feat(skills): add sniff finding subroutine to printing-press-amend Phase 1b\n\nDocument the opt-in sniff path that runs printing-press crowd-sniff (and\noptionally browser-sniff with a user-supplied HAR) against the target\nCLI's source URL, then converts each discovered candidate endpoint to a\nTier-3 add-endpoint finding with provenance: sniff.\n\nIncludes the six steps (resolve URL, crowd-sniff, optional browser-sniff,\nconvert to findings, degraded-path table, provenance surface at Phase 3).\nSniff is gated on an explicit kind: sniff ask — never auto-invoked.\n\nU4 of docs/plans/2026-05-16-001-feat-printing-press-amend-direct-input-mode-plan.md\n\n* feat(skills): add direct-input-parsing reference for printing-press-amend\n\nNew reference doc loaded by Phase 1b. Documents the ask-to-finding\nparsing rubric (six finding kinds), the finding shape (with new\nprovenance field), target-CLI resolution rules (regex extraction + Phase 0\nfallback), and edge cases (multi-CLI, ambiguous verbs, bare URLs,\nconflicting kinds, combined-mode merging).\n\nMirrors transcript-parsing.md structure so Phase 1a and Phase 1b reference\ndocs feel parallel.\n\nU5 of docs/plans/2026-05-16-001-feat-printing-press-amend-direct-input-mode-plan.md\n\n* docs(skills): v0.2 addenda to amend brainstorm + plan; add direct-input plan\n\nAppend a v0.2 amendment section to the original brainstorm and original\nv0.1 plan pointing at the new direct-input plan. Preserves both originals\nas the dogfood-mode design record; the v0.2 design (input-mode detection,\ndirect-input parsing, sniff finding type) lives in its own plan file.\n\nAdd docs/plans/2026-05-16-002-feat-printing-press-amend-direct-input-mode-plan.md\ncovering U1-U6 of the v0.2 expansion. Sequence number 002 to avoid\ncollision with the parallel-session bugbounty-goat plan filed today.\n\nU6 of docs/plans/2026-05-16-002-feat-printing-press-amend-direct-input-mode-plan.md\n\n* fix(skills): count only newly-added PATCH markers in printing-press-amend parity check\n\nGreptile P1: the parity check used grep -rc to count // PATCH(...) markers\nacross all Go files in $CLI_DIR, including markers from prior amend runs.\nThat cumulative count compared against the per-run declared patch_count\nsilently passed when prior history made the running total meet or exceed\nthe declared count, even with zero new markers added.\n\nSwitch to git format-patch --stdout + grep '^+.*// PATCH(' so the count\nreflects only newly-added markers in this run's diff against upstream.\nformat-patch is used over git diff to stay robust against environments\nwhere git diff is intercepted by a pager/shim that reformats unified-diff\noutput.\n\nResolves Greptile finding on PR #1490.\n\n* fix(skills): relabel printing-press-amend PR-body Evidence block\n\nGreptile P1: the Evidence block labeled .printing-press-patches.json as\n\"Plan doc\" and the next line said \"see the plan doc at the path above\"\n- the URL points at the patches JSON, not the plan doc, which lives\nlocally at $PRESS_MANUSCRIPTS/<slug>/<run-id>/proofs/... and is never\ncommitted to the public library.\n\nRelabel the URL as \"Patch record\", point per-finding rationale at the\nFindings table + patch record, and note the local plan doc location as\ncontext for the original printer (not as a clickable artifact for\nexternal reviewers). The plan doc stays local by design - it's a\nPII-scrubbed audit record, not part of the PR contract.\n\nResolves Greptile finding on PR #1490. Origin R27's requirement for a\nfull GitHub URL to the plan doc is unsatisfiable as written and is\nsilently revised here; flag for retro follow-up.\n\n* fix(skills): hard-stop existing-open-PR path in library-pr-plumbing\n\nGreptile P2: when existing_open was non-empty the snippet printed a\nwarning and a # AskUserQuestion: ... shell comment, then unconditionally\nran git checkout -b \"$BRANCH_NAME\" - which fails with a confusing\nalready-exists error because the local branch from the open PR exists.\n\nReplace the inert comment with a hard exit 1, a concrete two-option\nresolution menu printed to the user, and a follow-up prose block that\ndocuments the AskUserQuestion-then-re-enter contract the skill driver\nmust honor. Literal shell-flow execution now stops at the right place\nwith an actionable message.\n\nResolves Greptile finding on PR #1490.\n\n* fix(cli): mark --dir as required on verify-internal-skill\n\nGreptile P2: --dir was validated via a manual `if dir == \"\"` check in\nRunE, which works but doesn't surface the constraint in Cobra's --help\noutput and runs the validation later than necessary.\n\nReplace with cmd.MarkFlagRequired(\"dir\") after the StringVar\ndeclaration. Missing --dir now produces \"required flag(s) \\\"dir\\\" not\nset\" via Cobra's standard flow and the flag is annotated as required\nin --help output. The manual check is removed as redundant.\n\nResolves Greptile finding on PR #1490.\n\n* fix(skills): make 90-days-ago date portable in printing-press-amend\n\nGreptile P1: `date -v-90d` is BSD/macOS-only. On Linux it emits\n\"date: invalid option -- 'v'\" to stderr and produces empty stdout,\nleaving the GitHub search qualifier as `merged:>` with no value.\nThe recently-merged-PR dedup guard then silently returned no results,\ndefeating Phase 2a's purpose on any non-macOS machine.\n\nReplace with a three-way fallback: GNU `date -d '90 days ago'` first,\nBSD `date -v-90d` second, python3 timezone-aware UTC date as final\nfallback. Hard exit 1 if all three fail rather than letting the dedup\nguard silently drop out.\n\nResolves Greptile finding on PR #1490.\n\n* fix(skills): assign dogfood_status=N/A in direct-input mode\n\nGreptile P1 (re-review 18:50): the PR body template printed `- Dogfood:\n$dogfood_status` and the Phase 8 RESULT block included `dogfood_status:\n<PASS|FAIL|N/A>`, but Phase 4's output spec omitted the variable\nentirely and no phase instruction assigned it in direct-input mode.\nEvery PR from a direct-input run would show an empty `- Dogfood:` line.\n\nAdd dogfood_status to Phase 4's output spec with a per-mode contract:\n  - MODE=dogfood: PASS|FAIL from the dogfood validation step\n  - MODE=direct:  always N/A (no transcript to dogfood against)\n  - MODE=both:    PASS|FAIL on the transcript half of the findings\nDefault must be set by end of Phase 4 so Phase 7/8 never see empty.\n\nAlso add defensive ${dogfood_status:-N/A} in the PR body template as\nbelt-and-suspenders against any future phase forgetting to assign it.\n\nResolves Greptile finding on PR #1490.\n\n* fix(skills): force-checkout main on managed-clone refresh\n\nGreptile P1 (x2, SKILL.md:460 + library-pr-plumbing.md:86): if a prior\nrun aborted between Phase 4's file edits and Phase 7's commit, the\nmanaged clone is left on an amend branch with uncommitted changes in\n$CLI_DIR. The refresh block's `git checkout main` then fails because\nthose edits would be overwritten, and the subsequent `git reset --hard\nupstream/main` never runs - leaving the clone stuck in a broken state.\n\nAdd -f to discard local edits. The managed clone is documented as a\nscratch surface owned by the skill; any leftover edits are by definition\nabort residue that must be discarded before the next run reuses it.\nThis is consistent with the existing reset --hard immediately after.\n\nResolves Greptile finding on PR #1490.\n\n* fix(skills): use working-tree git diff for parity check (not format-patch)\n\nGreptile P1: my prior G1 fix used git format-patch --stdout\nupstream/main..HEAD, but the parity check runs in Phase 4 Step 6 -\nBEFORE Phase 7's commit. The edits live only in the working tree, so\nupstream/main..HEAD is an empty commit range and format-patch emits\nnothing. new_patch_markers is always 0:\n\n  - When patches_entry > 0 (any real patch run): 0 < patches_entry is\n    true and exit 1 fires unconditionally, blocking every valid run.\n  - When patches_entry = 0: the check silently passes regardless.\n\nSwitch back to working-tree git diff (the right tool for pre-commit\nstate). Add --no-pager + --no-color + --no-ext-diff to defeat\ncolorized output and any configured diff.external tool that would\nreformat the diff away from unified-diff shape.\n\nSmoke-tested the corrected snippet against a fresh repo with uncommitted\nPATCH-bearing edits: returns 1 for the 1 new marker, as expected.\n\nResolves Greptile finding on PR #1490 (refines G1's prior fix).\n\n---------\n\nCo-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"c10d0b55","date":"2026-05-16 13:02:01 -0700","author":"Trevin Chow","subject":"fix(cli): move extra_commands SKILL.md block to its own top-level section (#1524)","body":"* fix(cli): move extra_commands SKILL.md block to its own top-level section\n\nextra_commands declared in internal-YAML specs (shopify, producthunt,\n…) used to render under a **Hand-written commands** subsection nested\ninside ## Command Reference. The Cobra emitter never reads the field,\nso those rows were promises the generator could not back. verify-skill\nwalks ## Command Reference for inline backticks shaped like\n`<binary> <cmd>` and correctly fails the unknown-command check on\nevery entry.\n\nLift the block into its own top-level ## Hand-written Extensions\nsection with an explainer line that says they require separate\nwiring. verify-skill's _extract_inline_commands is scoped to the\nCommand Reference body (regex in scripts/verify-skill/verify_skill.py),\nso a sibling section is invisible to the unknown-command walker.\nGenerating from catalog/specs/producthunt-spec.yaml and running\n`printing-press verify-skill --dir <out>` now exits 0.\n\nCloses #1451\n\nSweep tracking issue for already-published CLIs (shopify, producthunt):\nmvanhorn/printing-press-library#616\n\n* fix(cli): place Hand-written Extensions after Finding the right command\n\nGreptile flagged that inserting `## Hand-written Extensions` between\nthe Command Reference body and Freshness Contract silently reparented\n`### Finding the right command` under the new section for any CLI\nwith extra_commands but no Freshness Contract (extra_commands +\nHasDataLayer=false case).\n\nMarkdown nests every `###` under the most recent `##`, so the fix is\npositional: render the Extensions block after the `### Finding the\nright command` subsection (and before `## Recipes`). `### Finding`\nstays a child of `## Command Reference` for every combination of\nExtraCommands + Freshness state. Added a regression assertion that\nlocks the relative ordering.\n\nEnd-to-end re-verified: producthunt regens with `## Command Reference`\n→ `### Finding the right command` → `## Hand-written Extensions` →\n`## Auth Setup`, and verify-skill still exits 0.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"99637941","date":"2026-05-16 12:49:20 -0700","author":"Trevin Chow","subject":"fix(skills): require codex completion marker before treating delegation as success (#1523)","body":"* fix(skills): require codex completion marker before treating delegation as success\n\nCodex `exec` can exit non-zero or be killed mid-run (sandbox abort, OOM,\nSIGINT, internal toolchain crash) and leave partial work behind that\nlooks identical to a successful end-of-prompt exit. The existing\npost-codex validate step only checks `go build && go vet` and a\nnon-empty `git diff`, both of which can pass against partial output, so\nthe parent skill silently treats partial completion as success and\nproceeds to the next priority task with the partial files in tree.\n\nEach of the four prompt templates now ends with a COMPLETION MARKER\nsection instructing Codex to write `_codex-result.json` (with\n`{status,files_written,timestamp}`) only after VERIFY succeeds. The\nDelegate step removes any stale marker before the codex invocation so a\nfile from a prior task can't fool the next one, and the Validate step\ntreats missing-or-not-complete marker as a Codex failure that falls\nthrough to the existing revert + Claude-fallback path and feeds the\ncircuit breaker.\n\nCloses #1288\n\n* fix(skills): make completion-marker validate snippet self-describing on failure\n\nPer Greptile review on PR #1523: the previous validate snippet chained\nthe marker check and the build step with `&&`, so a missing/incomplete\nmarker exited non-zero silently with the same exit code as a build\nfailure. The surrounding prose told the agent to surface a specific\nerror message, but the snippet itself never printed it, leaving the\nagent to second-guess which arm failed.\n\nRewrite both Validate snippets (Phase 3 step e, Phase 4 step 5) as an\n`if` block that echoes the exact `codex output marker missing` message\nto stderr before exiting non-zero. The build/vet branch is unchanged.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"82ad787b","date":"2026-05-16 12:29:12 -0700","author":"Trevin Chow","subject":"fix(cli): cap body-flag recursion depth to prevent compiler OOM (#1522)","body":"* fix(cli): cap body-flag recursion depth to prevent compiler OOM\n\nDeeply nested OpenAPI request bodies (Tripletex, NetSuite, SAP S/4HANA, and\nsimilar enterprise specs) produced POST/PUT command files with 40k+ lines as\nthe body-flag emitters recursed through every nested object. The Go compiler\nran out of memory (signal: killed) before finishing the package.\n\nCaps recursion at maxBodyFlagDepth = 3 across the four body-emitter helpers:\nrenderBodyMap, renderBodyVarDecls, renderBodyFlagRegs, renderBodyRequiredChecks.\nWhen the next depth would meet the cap, the object's subtree is skipped\nuniformly across all four; deeper fields are reachable via the existing\n--stdin flag on POST/PUT/PATCH commands.\n\nThe --stdin flag's help text is rewritten when truncation fires so an\noperator inspecting --help sees the affordance without reading the issue\ntracker. Behavior for shallow specs (<= 2 levels of nesting) is unchanged\nacross all 20 golden fixtures.\n\nCloses #1240\nRefs #1520\n\n* fix(cli): walk flattened body for depth-cap truncation signal\n\nbodyExceedsFlagDepth read endpoint.Body directly, but the four emitters\nwalk flattenCollidingBodyFields(endpoint.Body). When dot-flattened\nidentifier collision clears an object's Fields, the emitters treat that\nsubtree as a JSON-string leaf and never recurse past it, so no depth-cap\ntruncation actually occurs. The predicate, however, still saw the\noriginal deep structure and returned true, rewriting the --stdin help\ntext to the truncation-warning variant even though every field was\nexposed as a per-field flag.\n\nWalk flattenCollidingBodyFields in the predicate so detection and\nemission cannot drift. Adds a regression test for the\ncollision-flattened deep subtree case.\n\nRefs #1240\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"0c3c8bcc","date":"2026-05-16 12:04:00 -0700","author":"Trevin Chow","subject":"fix(cli): detect integer page paginator and advance numerically in sync (#1519)","body":"* fix(cli): detect integer page paginator and advance numerically in sync\n\nOpenAPI specs that paginate by integer ?page=N (Freshworks family,\nAtlassian Cloud, HubSpot, ~30-40% of REST APIs) used to fall through\ndetectPagination's \"after\" cursor default, so the generated sync loop\ntried to extract a body cursor that never existed and terminated after\npage 1.\n\nThe OpenAPI parser now recognizes page / page_number / pageNumber /\npage[number] as a Type=\"page\" paginator after the existing\ncursor/token/offset branches (cursor wins on mixed-form specs). The\nsync template adds a runtime fallback: when cursorParam == \"page\" and\nno body cursor is extracted but the page is full, advance the integer\ncounter so subsequent pages are fetched. Mirrored in the\ndependent-resource sync loop for parity.\n\nLink-header pagination (the other case called out in #1296) is a\nseparate code path (HTTP header parsing rather than body extraction)\nand stays a follow-up.\n\nCloses #1296\n\n* fix(cli): guard page-int sync fallback on cursor type, not param name\n\nGreptile flagged that the original guard `pageSize.cursorParam == \"page\"`\nonly fires for APIs that literally name the param `page`. The parser\nalso recognises `page_number`, `pageNumber`, and `page[number]` and\npreserves their original case in CursorParam, so those three variants\nwould silently skip the fallback and still truncate after page 1.\n\nPlumb the paginator class through the runtime: PaginationProfile gains\nCursorType (aggregated from endpoint.Pagination.Type), determine-\nPaginationDefaults emits both cursorParam (request-key name) and\ncursorType (\"page\"/\"cursor\"/…) into paginationDefaults, and the two\nfallback blocks compare on cursorType so every canonical spelling\nshares the same guard.\n\nGenerator test is now parametrised over all four spellings.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"ff562375","date":"2026-05-16 11:45:53 -0700","author":"Trevin Chow","subject":"fix(cli): drop MCP code-orch handler pre-marshal that base64-encoded mutating bodies (#1521)","body":"Sibling fix to #1357. The code-orchestration MCP handler template\n(mcp_code_orch.go.tmpl) had the same defect the endpoint-mirror template\nalready fixed: POST/PUT/PATCH branches json.Marshaled `params` into a\n[]byte, then handed the []byte to c.Post/Put/Patch as the body. The\ngenerated client.do() then json.Marshaled the []byte again, which\nencoding/json base64-encodes into a quoted string. Strict APIs reject\nthe payload with errors like 'Request data must be a JSON object, not a\nstring'.\n\nPass `params` (map[string]any) directly to c.Post/Put/Patch and let\nclient.do() do the single marshal pass. The intermediate marshal-error\nbranch is removed; client.do() already wraps any marshal error with the\nsame \"marshaling body\" prefix (client.go.tmpl:878).\n\nAdd TestMCPCodeOrchPassesParamsMap mirroring TestMCPHandlerPassesBodyArgsMap\nto pin the orchestrator emission shape: no json.Marshal(params)\nintermediate, and the literal forms c.Post(path, params),\nc.Put(path, params), c.Patch(path, params).\n\nGolden fixture mcp-cloudflare/internal/mcp/code_orch.go updated in\nlockstep per AGENTS.md \"Generator Output Stability\".\n\nCloses #1426."},{"hash":"95417401","date":"2026-05-16 11:10:14 -0700","author":"Trevin Chow","subject":"fix(cli): filter auth login --chrome cookies by spec auth.cookies allowlist (#1518)","body":"* fix(cli): filter auth login --chrome cookies by spec auth.cookies allowlist\n\nThe composed-auth branch of `auth login --chrome` already kept only the\ncookies named in spec.auth.cookies before composing the Authorization\nheader. The non-composed cookie-auth branch ignored that allowlist and\nsaved every cookie the target domain had set into `access_token`. The\nruntime then sent the whole blob (CSRF tokens, WAF cookies, anti-bot\nfingerprints alongside the real credential) on every authenticated\nrequest, routinely tripping upstream WAFs and shadowing the credential\nin the Cookie header.\n\nApply the same allowlist filter in the non-composed branch: parse the\nextracted blob, keep only the names declared in spec.auth.cookies, and\nrejoin before SaveTokens. A missing required cookie now errors with a\nre-login prompt instead of silently saving a bad blob. Specs that\ndeclare no allowlist (legacy cookie-auth CLIs) keep their current\nbehavior unchanged.\n\nCloses #1292\n\n* fix(cli): filter cookie blob in refreshStoredBrowserCookies too\n\nGreptile flagged that the login-side allowlist filter could be silently\nundone by `auth refresh` on CLIs that combine `auth.type: cookie`,\n`auth.cookies: [name]`, and `auth.requires_browser_session: true`. The\nrefresh helper runs unattended and was saving the raw extracted blob,\noverwriting the filtered token that login had just persisted.\n\nApply the same parse-filter-rejoin block to the non-composed branch of\nrefreshStoredBrowserCookies. Specs without an allowlist still take the\nexisting code path. Added a subtest that pins the refresh-path filter on\na requires_browser_session spec.\n\nRefs #1292\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"34ca8149","date":"2026-05-16 13:49:10 -0400","author":"Michael Schreiber","subject":"feat(cli): add --auth-preference flag for catalog-driven scheme selection (#865)","body":"* feat(cli): add --auth-preference flag for catalog-driven scheme selection\n\nWhen an OpenAPI spec advertises multiple security schemes — most commonly\nOAuth2 (with full authorizationCode flow) plus HTTP Basic — the parser\ndefaulted to OAuth2. That selection is correct for hosted multi-tenant\nintegrations but unusable for personal-token CLIs (Atlassian Jira /\nConfluence / Bitbucket, GitLab, and others all expose this exact pair).\n\nAdd a generation-time hint that lets a catalog entry — or a one-off\ngenerate caller — pin a specific securityScheme name from the spec:\n\n  printing-press generate --spec jira.json --auth-preference basicAuth\n\nThe hint is a soft preference: an unknown name silently falls back to\nthe existing default selector so a typo in catalog YAML degrades\ngracefully rather than failing the whole generate. Matching is\ncase-insensitive against the spec's scheme name.\n\nPlumbing:\n\n* internal/openapi/parser.go: introduce ParseOptions, route the six\n  Parse*/ParseFile* helpers through a single ParseWithOptions, thread\n  AuthPreference into selectSecurityScheme.\n* internal/cli/root.go: --auth-preference flag on `generate`.\n* internal/catalog/catalog.go: optional auth_preference field on Entry.\n* .github/workflows/validate-catalog.yml: forward auth_preference to\n  generate so the CI matches what users get from the catalog.\n* catalog/jira.yaml: new official entry, pinned to basicAuth.\n\nTests cover: preference selects the named scheme, case-insensitive\nmatching preserves the spec's casing in Auth.Scheme, and an unknown\npreference falls through to the default OAuth2-wins behavior.\n\nCo-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n\n* fix(ci): use yq to read auth_preference in validate-catalog\n\ngrep+sed silently mangled quoted values, trailing comments, or\nindented keys. Use `yq e '.auth_preference // \"\"'` so the workflow\nforwards the same value the generator would parse.\n\nCo-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n\n* fix(cli): forward catalog auth_preference into OpenAPI parse for generate\n\n* test(cli): migrate auth-preference test fixtures from bare example.com to api.example.com\n\nThe spec validator added in main (fix #984) rejects bare example.com as a\nreserved RFC 2606 placeholder host; subdomains like api.example.com remain\nallowed. Update the two auth-preference parser tests so they pass the merged\nvalidator.\n\nCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>\n\n* fix(cli): flatten nested-prefix body fields that collide with sibling identifiers\n\nTwo body properties whose camelCased prefix-paths converge on the same\nGo identifier — e.g. Atlassian ProjectComponent's top-level\n`leadAccountId` alongside the sibling `lead` object's nested\n`accountId` — emit two `var bodyLeadAccountId string` declarations\nafter #957's nested-object expansion, and the generated CLI fails to\ncompile with \"redeclared in this block\".\n\nThe existing parser-side seenCamelNames pass only dedupes among\ntop-level body params; it doesn't predict the identifiers that the\ngenerator's nested-prefix recursion will produce.\n\nAdd flattenCollidingBodyFields, a generator-side pre-pass that walks\nendpoint.Body using the same identifier rule as renderBodyMap\n(`toCamel(paramIdent(p))` joined to the parent prefix). Where a leaf's\npredicted identifier collides with another leaf elsewhere in the tree,\nclear the offending parent's Fields so it falls through to the\nJSON-string fallback. The user can still reach the nested data via\n`--lead '{\"accountId\":\"...\"}'`.\n\nWired into all four body-emission template helpers (bodyMap,\nbodyVarDecls, bodyFlagRegs, bodyRequiredChecks) so detection and\nemission cannot drift. No effect on the common case: 17/17 golden\nfixtures byte-identical.\n\nDiscovered while greening up the validate-catalog CI job for the Jira\ncatalog entry added in this PR — the Atlassian Jira spec was the first\nreal shape to trip the collision.\n\nCo-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n\n* fix(cli): preserve Bearer default over OAuth2 authorization-code in scheme selection\n\nThe OAuth2+AC early-return loop in selectSecurityScheme bypassed the\nscoring system for any spec advertising both http/bearer and OAuth2\nauthorizationCode. Since schemePriorityBearer = 0 < schemePriorityOAuth2AuthCode\n= 200, the scoring system intentionally picks Bearer as the simpler\nscheme for CLI use; the early-return inverted that for every Bearer +\nOAuth2+AC spec.\n\nThe loop also added nothing for the Jira goal: AuthPreference already\npins basicAuth when the catalog asks, and scoring already prefers\nOAuth2+AC (200) over basicAuth (500) by default.\n\nDrop the loop. Add a parser test that locks both halves of the contract:\ndefault selection keeps Bearer; AuthPreference: \"OAuth2\" still lets\ncallers opt into the 3LO dance when they want it.\n\n---------\n\nCo-authored-by: Claude Opus 4.7 <noreply@anthropic.com>\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>\nCo-authored-by: Trevin Chow <trevin@trevinchow.com>"},{"hash":"62838d04","date":"2026-05-16 10:38:03 -0700","author":"Trevin Chow","subject":"fix(cli): validate-narrative splits piped recipes and strips redirects (#1517)","body":"* fix(cli): validate-narrative splits piped recipes and strips redirects (#1455)\n\nRecipes that pipe a CLI invocation into jq/head/xargs (or feed input via\n`< file`) used to fail validate-narrative wholesale — every shell-shaped\ncommand was classified as Unsupported with a \"pipe-skipped\" reason, so\nrealistic narrative recipes had to drop their tails to ship.\n\nTeach the splitter to:\n- Split on top-level `|` (in addition to `&&`, `||`, `;`) and tag any\n  segment that sits to the right of a pipe as AfterPipe so the validator\n  skips it with a `pipe-skipped:` note instead of failing the recipe.\n- Strip top-level `<`, `>`, `>>` redirects from each runnable segment\n  before validation, recording each removed token as a\n  `redirect-stripped:` note. Redirects inside quotes and `2>&1`-style fd\n  duplications are left intact.\n\nResult is reflected on Result.Notes (new field). Recipes like\n`yt-cli sync --json | jq | head -c 2000` and `cli bulk --stdin < keys.txt`\nnow validate against their leading segment and ship clean, matching the\nacceptance criteria in #1455.\n\nExtends #1271, which handled `&&`/`;` and explicitly deferred `|` as a\nknown limitation.\n\n* fix(cli): stripRedirects respects digit-prefix fd dup + shell quoting (#1455)\n\nAddresses two P2 Greptile findings on #1517:\n\n1. `>&file` (POSIX shorthand for stdout+stderr to a file) was being\n   left intact because the fd-duplication guard fired on any `>&`.\n   The signal is actually a *preceding* digit AND a following `&`\n   (`2>&1`, `1>&2`). Check both before bypassing the strip.\n2. Quoted redirect targets like `< 'file with spaces.txt'` were\n   truncated at the first space inside the quote. Walk the file\n   token with the same single/double-quote and backslash awareness\n   that the outer scanner uses so the full quoted name lands in the\n   redirect-stripped note.\n\n* test(cli): order Result.Notes left-to-right + cover reset operators (#1455)\n\nAddresses two follow-up Greptile findings on PR #1517:\n\n- Collect pipe-skipped and redirect-stripped notes in a single\n  left-to-right pass over segments so a recipe like\n  \\`cmd <file | jq\\` records the redirect note before the pipe note,\n  matching the source token order. Adds\n  TestValidate_MixedRedirectAndPipeNotesAreLeftToRight to pin the\n  contract.\n- Extend TestSplitShellChain with cases for \\`||\\` and \\`;\\` after a\n  pipe so the afterPipe reset for those operators is covered alongside\n  the existing \\`&&\\` case. All three share the same reset code path;\n  the new cases close the silent-regression gap.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"f5ea270d","date":"2026-05-16 13:25:49 -0400","author":"klatt42","subject":"fix(cli): match both singular and plural required-flag cobra outputs (#1413)","body":"isCobraUsageError matched only the plural cobra form (`required flag(s)\n\"foo\" not set`). Cobra emits the SINGULAR form (`required flag \"foo\"\nnot set`, no parens) when exactly one MarkFlagRequired flag is missing\non the command, and only switches to plural when multiple are missing\nsimultaneously. The single-flag case is by far the more common one in\npractice.\n\nResult before this fix: a CLI with one required flag, invoked without\nit, exits 1 instead of the documented exit-2 usage convention from\n#1194. The unknown-flag / unknown-command / flag-needs-argument paths\nall worked correctly; only this single edge case fell through.\n\nAdds the singular pattern alongside the plural one. Both stay anchored\nto the literal trailing-space-quote (`required flag \"`) so application\nerrors mentioning \"required flag\" as prose still classify correctly\n(the prose-as-non-usage negative test continues to pass).\n\nDiscovered while regenerating rok-brain-cli against v4.6.1 — the\nsingle-required-flag case in Brain's `query --query \"...\"` command\nsurfaced the gap because `query` has exactly one MarkFlagRequired flag.\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"43c90ba0","date":"2026-05-16 18:11:46 +0100","author":"liam-ai-reality","subject":"feat(catalog): add OpenRouteService under new maps category (#1513)","body":"* feat(catalog): add OpenRouteService under new maps category\n\nAdds OpenRouteService (ORS) to the embedded catalog under a new `maps`\ncategory, the first map / routing / VRP entry. Backed by a hand-crafted\nOpenAPI 3.0 spec covering directions, matrix, isochrones, optimization,\ngeocode forward/reverse.\n\n- New `maps` category registered in internal/catalog/catalog.go,\n  internal/catalog/catalog_test.go, and the AGENTS.md prose enumeration.\n- catalog/openrouteservice.yaml (tier: community, spec_source: docs).\n- catalog/specs/openrouteservice-spec.yaml — hand-crafted OpenAPI 3.0.3,\n  11 schemas, 3 reusable responses, 9 routing profiles incl driving-hgv.\n- Golden update for catalog-list/stdout.txt (+3 lines: maps block).\n\nRefs #1512\n\nVerified locally:\n  go test ./internal/catalog/...   # 71 pass\n  go build ./...                   # clean\n  go vet ./...                     # clean\n  ./scripts/golden.sh verify       # 20 / 20 pass\n\n* fix(cli): clarify openrouteservice auth notes\n\n* chore(catalog): drop sandbox_endpoint and OPENROUTESERVICE_API_KEY alias\n\n- sandbox_endpoint == prod for ORS (no separate test endpoint). Removed.\n- ORS_API_KEY is the canonical env var used by ors-py / ors-js; alias\n  OPENROUTESERVICE_API_KEY is verbose and not used by any existing\n  community tool. Removed.\n\nRefs #1512\n\n* Update catalog/specs/openrouteservice-spec.yaml\n\nCo-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>\n\n* Update catalog/specs/openrouteservice-spec.yaml\n\nCo-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>\n\n* Update catalog/specs/openrouteservice-spec.yaml\n\nCo-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>\n\n---------\n\nCo-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>"},{"hash":"bf9014d4","date":"2026-05-16 03:15:10 -0700","author":"Trevin Chow","subject":"fix(cli): synthesize undeclared path placeholders in OpenAPI parser (#1510)","body":"* fix(cli): synthesize undeclared path placeholders in OpenAPI parser (#1467)\n\nReal-world OpenAPI specs (Tally, others) frequently include {placeholder}\ntokens in a path template without declaring the corresponding parameter\nat either the operation or path-item level. The YAML loader already\nhandles this via enrichEndpointPathParams; the OpenAPI parser did not.\nWith no matching Param entry the generator emits a literal\n`/organizations/{organizationId}/invites` URL and every request returns\n404 — no flag, no positional, no substitution call.\n\nExport enrichPathParams as EnrichPathParams on APISpec and invoke it\nfrom the OpenAPI parser just before Validate, so OpenAPI-parsed specs\nget the same path-placeholder synthesis the YAML loader provides. The\nexisting case for already-declared params (e.g. {x} declared as query)\nis reused too, which fixes a sibling case where a placeholder declared\nin the wrong location still drove a 404.\n\n* test(cli): scan by name instead of asserting exact Params count (#1467)\n\nAddresses Greptile review feedback on #1510. require.Len(Params, 1)\nwould produce a confusing mismatch if any future enrichment step\nsynthesized an additional param. Scan for organizationId by name so the\nassertion stays meaningful regardless of unrelated param growth.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"c7e1b82b","date":"2026-05-16 02:52:06 -0700","author":"Trevin Chow","subject":"fix(cli): preserve body/content payload on single-object compact path (#1509)","body":"* fix(cli): preserve body/content payload on single-object compact path\n\nThe single helpers.go map compactVerboseFields was consulted by both\ncompactListFields and compactObjectFields. On a list, \"body\"/\"content\"/\n\"html\"/\"markdown\" are verbose noise and stripping them is right. On a\nsingle-object `get --agent` response those fields are the primary\npayload, and stripping them silently returns a useless envelope with\nno error and no stderr signal.\n\nSplit the map into compactVerboseListFields (unchanged contents) and\ncompactVerboseObjectFields (description/comments/attachments only),\nso compactObjectFields preserves payload fields while list compaction\nbehaves exactly as before. Agents that still want to omit body/content\non `get` can pass `--select`.\n\nCloses #1481\n\n* test(cli): use balanced-brace scan to bound compactVerboseObjectFields literal\n\nGreptile flagged that strings.Index(body[objStart:], \"}\") returns the\nfirst `}` after the map declaration, which works today (no intermediate\nbraces) but would silently truncate objBody if a future edit added a\nfield whose value contained `}` or an inline comment with `}`, causing\nthe NotContains assertions to give false-positive passes.\n\nReplace with a small matchClosingBrace helper that counts brace depth\nfrom the first `{` and returns the index of the matching close. Same\ntest semantics; resilient to future map shape changes.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"fe4a5ec3","date":"2026-05-16 02:26:00 -0700","author":"Trevin Chow","subject":"feat(cli): catalog auth_env_vars declares canonical credential env vars (#1508)","body":"* fix(skills): surface novel-feature hand-code scope at Phase Gate 1.5\n\nCloses #1450\n\nPhase Gate 1.5 lacked a signal that approving \"N novel features scored\n>=5/10\" committed the agent to ~50-150 LoC of hand-written Go per\nfeature, plus root.go wiring, for every transcendence row the generator\nwon't auto-emit. Users approved without seeing that scope, and the gap\nsurfaced later as validate-narrative / verify-skill failures.\n\nTwo coordinated edits:\n\n* novel-features-subagent.md: Pass 3 gains a fifth force-answer question\n  (\"Buildability\"), tagging each survivor `spec-emits` or `hand-code`.\n  The output contract requires the survivors table to extend the\n  rubric's transcendence format with a Buildability column; the\n  SKILL-side handler reads that column for the gate showcase.\n\n* SKILL.md Phase Gate 1.5: the prose showcase now covers four\n  must-haves, with a new \"Hand-code commitment\" item that states the\n  auto-emitted vs hand-code split and lists the hand-code feature names\n  before the AskUserQuestion fires.\n\n* fix(skills): align Step 1.5c transcendence table template with Buildability column\n\nGreptile P1 on PR #1501: the canonical four-column transcendence table\ntemplate at Step 1.5c didn't carry the new Buildability column, so an\nagent writing the manifest from that template would silently drop the\ncolumn and Phase Gate 1.5's hand-code count would lose its source of\ntruth in the manifest. Update the template to include Buildability and\nfix the stale \"already matches\" wording.\n\n* fix(skills): add Buildability column to rubric Transcendence Table Format\n\nGreptile 2nd-pass on PR #1501: the subagent reads absorb-scoring.md as\nits operational playbook, so the canonical Transcendence Table Format\nalso needs the new Buildability column. Without it, the subagent has no\nguidance on where to place the column in its Survivors output, leaving\nthe manifest parse step's Buildability lookup brittle.\n\nClarify the relationship between the existing \"How It Works\" column (the\nbuildability proof sentence) and the new \"Buildability\" column (the\nspec-emits / hand-code tag) so the two distinct concepts don't get\nconflated.\n\n* fix(skills): tighten Buildability wording per PR review\n\nTwo minor wording cleanups flagged in Greptile's 5/5 review:\n\n* novel-features-subagent.md Output contract: now that absorb-scoring.md\n  also includes Buildability, \"extends with one additional column\" is\n  self-contradictory. Switch to \"matching the rubric's format (which\n  includes the Buildability column)\".\n\n* SKILL.md Phase Gate 1.5: by gate time the agent reads the manifest,\n  not the archived brainstorm. Point at the manifest transcendence\n  table's Buildability column as the source of truth.\n\n* feat(cli): catalog auth_env_vars declares canonical credential env vars\n\nCatalog entries can now declare auth_env_vars: an ordered list of\ncanonical credential env var names (STRIPE_SECRET_KEY, GITHUB_TOKEN, etc.).\nThe generator merges them in front of the parser's name-derived default and\nemits config.go reading each in declared order, so an operator who exports\nany one satisfies auth. The parser's name-derived default trails as a\nbackwards-compat fallback so existing setups don't need a rename.\n\nCatalog-mode generation (printing-press generate <name>) bypasses the\nspec-edit step the SKILL's Pre-Generation Auth Enrichment uses, so this\nis the only way to declare canonical names without hand-patching the\ngenerated CLI on every regen.\n\nSeeded for stripe, github, discord, sentry. Stytch (HTTP Basic pair)\nintentionally deferred — basic auth treats env vars as a credential\npair, not as alternatives, so the override is a no-op for basic\nauth.Format shapes; declare basic-auth pairs via x-auth-env-vars on\nthe security scheme instead.\n\nCloses #1482\n\n* fix(cli): reject whitespace-padded auth_env_vars at validation time\n\nGreptile P2 from PR #1508 review: the validator trimmed each name before\nrunning the regex check but stored the original (untrimmed) string in\nentry.AuthEnvVars, so a YAML entry like \"  STRIPE_KEY  \" passed\nvalidation and was only normalized at runtime in mergeAuthEnvVars. Add\nan explicit whitespace-rejection step so the stored slice always matches\nwhat was validated, with a distinct error message that points at the\nspecific YAML hygiene issue rather than the regex shape.\n\nRefs #1482\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"bf7f3465","date":"2026-05-16 02:05:21 -0700","author":"Trevin Chow","subject":"fix(cli): route promoted-command file stem through safeResourceFileStem (#1489) (#1506)","body":"PR #1021 fixed `safeResourceFileStem` to suffix `_cmd` on stems ending in\nGOOS/GOARCH/test tokens, but only for the `<resource>_<verb>.go` emit\npath. The promoted-command emitter at generator.go:2605 builds its path\nas `\"promoted_\" + pc.PromotedName + \".go\"` without the safe-stem wrapper,\nso an OpenAPI resource named `test` produces `internal/cli/promoted_test.go`.\nGo treats *_test.go as a test file and excludes it from the normal package\nbuild, leaving root.go's `newTestPromotedCmd` reference undefined.\n\nWrap the stem in `safeResourceFileStem`. The function's `_test`/GOOS/GOARCH\ntrailing-token rules now cover both emit paths.\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"f64e9913","date":"2026-05-16 01:47:18 -0700","author":"Trevin Chow","subject":"fix(skills): drop cli-skills mirror regen from publish flow (#1502)","body":"* fix(skills): drop cli-skills mirror regen from publish flow\n\nThe publish skill previously ran the library's generate-skills tool in\nStep 6 and committed the regenerated cli-skills/pp-<api-slug>/SKILL.md\nmirror, which made every fork PR fail the library's\n`Guard against hand-edits to cli-skills mirror` workflow. The auto-fix\nthat would otherwise rewrite the mirror via the library bot is gated on\n`head.repo.full_name == github.repository`, so fork PRs had no path that\nsatisfied both Guard and the drift check.\n\nDrop the regen call, drop `cli-skills/` from the staging `git add`, and\nflip the preamble to forbid touching the mirror in publish PRs. Replace\nthe regen lines with a guard comment naming the upstream Guard check and\npointing at printing-press-library#590 so the regen isn't reintroduced\nwithout addressing the library side. Update the contract test to assert\nthe new shape.\n\nCloses #1474\n\n* docs(cli): align AGENTS.md with new publish-skill contract; drop ticket URL from SKILL guard comment\n\nGreptile P1 + P2 from PR #1502.\n\nP1: AGENTS.md described the publish skill as driving cli-skills mirror\nregen and only prohibited editing registry.json or README catalog cells.\nThe publish-flow change in the prior commit makes both descriptions\nstale. Drop the regen reference from the skill's responsibility list and\nadd cli-skills/pp-<api-slug>/SKILL.md to the prohibited-edit list, with\na one-sentence pointer to the upstream Guard check that enforces it.\n\nP2: The Step 6 guard comment ended with a ticket URL, which the Code &\nComment Hygiene rules forbid (no ticket numbers in code comments;\ncomments must be self-contained). Replace the URL with a self-contained\ncondition for when the guard could be lifted.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"2abbfec0","date":"2026-05-16 03:32:14 -0500","author":"kberger111","subject":"fix(scorer): resolve binary at build/stage/bin/ before cliDir (#1150) (#1412)","body":"* fix(scorer): resolve binary at build/stage/bin/ before cliDir (#1150)\n\nThe Sample Output Probe was looking for the printed CLI's binary at\n<cliDir>/<name>(.exe), but the generator's --validate \"build runnable\nbinary\" gate emits the binary at <cliDir>/build/stage/bin/<name>(.exe).\nOn every printed CLI in the catalog the probe missed and reported\n\"is not executable\" — a scorer-calibration false alarm, not a real\ndefect.\n\nExpand liveCheckBinaryCandidatesForGOOS to try the canonical staged\npath first, then fall back to the legacy cliDir layout. This subsumes\nthe original .exe-suffix scope of #1150 (already partially fixed via\nplatform.ExecutablePathForGOOS) and addresses piercekiltoff's wider\nfinding from the servicetitan-crm retro: it's the directory, not just\nthe suffix.\n\nResolution order:\n  1. <cliDir>/build/stage/bin/<name>     canonical Unix\n  2. <cliDir>/build/stage/bin/<name>.exe canonical Windows\n  3. <cliDir>/<name>                     legacy fallback\n  4. <cliDir>/<name>.exe                 legacy Windows fallback\n\nAdds TestLiveCheckBinaryCandidatesPreferBuildStageBin to lock the\norder in.\n\nCo-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>\n\n* test(scorer): strengthen candidate ordering + add build/stage/bin integration test\n\nPer Greptile review on #1412:\n\n1. Ordering test now asserts all four cross-product positions:\n   stagedUnix < legacyUnix, stagedUnix < legacyWin,\n   stagedWin  < legacyUnix, stagedWin  < legacyWin.\n   Previously only stagedUnix < legacyUnix was checked, leaving\n   a silent window for reorder regressions on the Windows paths.\n\n2. TestLiveCheck_FindsBinaryInBuildStageBin exercises the full\n   RunLiveCheck probe with the stub binary placed at\n   build/stage/bin/<name> — the canonical post-v4.5.1 layout.\n   Skipped on Windows (shell-script stubs not supported there,\n   matching the existing integration-test convention).\n\nCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>\n\n---------\n\nCo-authored-by: Ken Berger <kberger@megadatahs.com>\nCo-authored-by: Claude Opus 4.7 <noreply@anthropic.com>\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"793d5b27","date":"2026-05-16 01:19:57 -0700","author":"Trevin Chow","subject":"fix(skills): surface novel-feature hand-code scope at Phase Gate 1.5 (#1501)","body":"* fix(skills): surface novel-feature hand-code scope at Phase Gate 1.5\n\nCloses #1450\n\nPhase Gate 1.5 lacked a signal that approving \"N novel features scored\n>=5/10\" committed the agent to ~50-150 LoC of hand-written Go per\nfeature, plus root.go wiring, for every transcendence row the generator\nwon't auto-emit. Users approved without seeing that scope, and the gap\nsurfaced later as validate-narrative / verify-skill failures.\n\nTwo coordinated edits:\n\n* novel-features-subagent.md: Pass 3 gains a fifth force-answer question\n  (\"Buildability\"), tagging each survivor `spec-emits` or `hand-code`.\n  The output contract requires the survivors table to extend the\n  rubric's transcendence format with a Buildability column; the\n  SKILL-side handler reads that column for the gate showcase.\n\n* SKILL.md Phase Gate 1.5: the prose showcase now covers four\n  must-haves, with a new \"Hand-code commitment\" item that states the\n  auto-emitted vs hand-code split and lists the hand-code feature names\n  before the AskUserQuestion fires.\n\n* fix(skills): align Step 1.5c transcendence table template with Buildability column\n\nGreptile P1 on PR #1501: the canonical four-column transcendence table\ntemplate at Step 1.5c didn't carry the new Buildability column, so an\nagent writing the manifest from that template would silently drop the\ncolumn and Phase Gate 1.5's hand-code count would lose its source of\ntruth in the manifest. Update the template to include Buildability and\nfix the stale \"already matches\" wording.\n\n* fix(skills): add Buildability column to rubric Transcendence Table Format\n\nGreptile 2nd-pass on PR #1501: the subagent reads absorb-scoring.md as\nits operational playbook, so the canonical Transcendence Table Format\nalso needs the new Buildability column. Without it, the subagent has no\nguidance on where to place the column in its Survivors output, leaving\nthe manifest parse step's Buildability lookup brittle.\n\nClarify the relationship between the existing \"How It Works\" column (the\nbuildability proof sentence) and the new \"Buildability\" column (the\nspec-emits / hand-code tag) so the two distinct concepts don't get\nconflated.\n\n* fix(skills): tighten Buildability wording per PR review\n\nTwo minor wording cleanups flagged in Greptile's 5/5 review:\n\n* novel-features-subagent.md Output contract: now that absorb-scoring.md\n  also includes Buildability, \"extends with one additional column\" is\n  self-contradictory. Switch to \"matching the rubric's format (which\n  includes the Buildability column)\".\n\n* SKILL.md Phase Gate 1.5: by gate time the agent reads the manifest,\n  not the archived brainstorm. Point at the manifest transcendence\n  table's Buildability column as the source of truth.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"ca4898a3","date":"2026-05-16 01:15:25 -0700","author":"Trevin Chow","subject":"fix(cli): apply Bearer prefix in composed and cookie AuthHeader paths (#1500)","body":"* fix(cli): apply Bearer prefix in composed and cookie AuthHeader paths\n\nThe generator's composed and cookie auth branches in `AuthHeader()` returned\nthe raw env-var token or chrome-imported AccessToken without applying the\nspec's HeaderPrefix, so any CLI configured via env-var fallback hit 401 on\nevery call (`Authorization: <raw>` instead of `Authorization: Bearer <raw>`).\nThe `auth login --chrome` path masked this because the token exchanged\nthrough Clerk already had the prefix; the env-var path documented in the\nSKILL did not.\n\nComposed and cookie branches now wrap returned tokens in a new\n`ensureAuthScheme(scheme, token)` helper that applies `Auth.HeaderPrefix`\n(defaulting to `Bearer`) and skips the prefix when the value already\ncarries it case-insensitively. This handles users who pre-prefixed their\nenv var to work around the bug without double-prefixing.\n\nbearer_token/api_key/oauth2 branches are untouched.\n\nCloses #1419\n\n* test(cli): extend ensureAuthScheme runtime test to cover cookie auth\n\nRefactored TestAuthHeaderComposedSchemeHelperHandlesPreprefixedTokens into\na table-driven test that runs the same three runtime assertions (Bearer\napplied, no double-prefix, blank returns empty) for both composed and\ncookie auth types. The cookie branch was previously only covered by source\ninspection, leaving a gap where a wrong scheme argument in the cookie\nAccessToken path could silently emit a bare token.\n\nAlso dropped Format: \"Bearer {token}\" from the runtime test spec. The\ncomposed and cookie branches in AuthHeader() do not consult Auth.Format —\nthe Bearer prefix comes from HeaderPrefix()'s default — and keeping\nFormat in the spec implied otherwise.\n\n* test(cli): drop fragile findReturnEnd slice in favor of whole-file checks\n\nThe findReturnEnd helper sliced the emitted config.go up to the first\n\"return \" in each if-block to bound the NotContains check. That heuristic\nsilently shortens the slice if the emitted block ever contains the literal\ntext \"return \" before the actual return statement (e.g. an AuthSource\nstring value), letting a regression slip through.\n\nReplaced with whole-file NotContains for the raw-return literals plus\npositive Contains for the ensureAuthScheme wrapper call. The raw return\nliteral should never appear in correctly emitted composed/cookie config,\nso the file-wide check is both simpler and tighter, and the positive\nassertion catches the wrong-helper-argument case the previous test was\ntrying to defend against.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"f3f70435","date":"2026-05-16 01:01:01 -0700","author":"Trevin Chow","subject":"fix(cli): refresh auth on redirects via CheckRedirect (#1497)","body":"* fix(cli): wire CheckRedirect to refresh auth on each redirect hop\n\nGenerated client.go now sets http.Client.CheckRedirect inside New() so\nthe closure can call c.authHeader() and re-stamp the auth header on\neach redirect. Go's default follows redirects and replays the original\nAuthorization header verbatim, which trips nonce-bound auth (OAuth 1.0a\nPLAINTEXT, SigV4, Hawk): the duplicate nonce hits the server's replay\ndetector and returns 401 on the second hop.\n\nFor static schemes (Bearer, api_key in header) c.authHeader() returns\nthe same value, so post-redirect headers are byte-identical. For\nnonce-bound schemes the call recomputes a fresh signature. Tier-routing\nselects header-vs-query at runtime, so its branch only caps depth and\nleaves Go's default replay in place. Query-param auth is preserved by\nGo's URL resolution on redirects, so its branch also just caps depth.\n\nCloses #1410\n\n* fix(cli): use plain error for redirect depth cap, not ErrUseLastResponse\n\nReturning http.ErrUseLastResponse from CheckRedirect makes Client.Do\nreturn the 3xx redirect response with a nil error. The generated do()\nfunction then classifies the 3xx as a successful response and hands the\nHTML \"Moved Permanently\" body back to the caller as if it were a valid\nAPI response. Match Go's defaultCheckRedirect by returning a plain\nerror so Do() surfaces it through do()'s err != nil branch.\n\nUpdates the regression test to match.\n\n* fix(cli): gate redirect auth re-stamp on same-host\n\nHeaders set inside CheckRedirect are sent to the redirect target\nverbatim — Go's automatic Authorization stripping\n(shouldCopyHeaderOnRedirect) only runs before CheckRedirect is invoked.\nRe-stamping the auth header unconditionally would leak the user's API\ntoken to a third-party host on cross-domain 3xx (open redirect or\npartner handoff). Gate the re-stamp on req.URL.Host == via[0].URL.Host\nso the nonce-replay fix still applies to same-host redirects (the\nSchoology 303 case) while preserving Go's cross-domain credential\nprotection.\n\nThe session_handshake header-mode branch carries the same gate.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"dc0651ab","date":"2026-05-16 00:15:27 -0700","author":"Trevin Chow","subject":"fix(cli): exempt vendor OpenAPI/Swagger source under .manuscripts from PII scan (#1496)","body":"* fix(cli): exempt vendor-published OpenAPI/Swagger source under .manuscripts from PII scan\n\nThe PII gate already exempts root-level spec.{json,yaml,yml} on the\npremise that vendor `example:` blocks are documentation, not customer\nPII. The exemption was depth-1 only, so a generation that archived the\nvendor's published per-resource spec files (`apps/calendars.json`,\n`pushpress-v3.yaml`, etc.) under .manuscripts/.../research/ false-failed\npublish on the documentation-shape values inside.\n\nAdds a content-detected variant of the same exemption: when a file\nanywhere inside a .manuscripts/ subtree begins with an OpenAPI 2.x/3.x\nor Swagger 2.0 root-document marker, scanPIIFile skips it. Content\ndetection beats filename heuristics because vendors ship spec source\nunder arbitrary basenames that no glob would reliably cover.\n\nThe exemption is scoped to .manuscripts/ so non-archive paths\n(committed docs/, generated internal/, testdata/ fixtures) still scan.\nHARs, session-state captures, and hand-edited proofs under\n.manuscripts/ also keep scanning because they carry no version marker.\nA complementary regression test pins both directions.\n\nCloses #1356\n\n* fix(cli): anchor JSON vendor-spec markers to document root\n\nGreptile flagged that the JSON markers (`\"openapi\"\\s*:\\s*\"[23]\\.`) had\nno document-root anchor, so a non-spec JSON file under .manuscripts/\nwith a nested `\"openapi\": \"3.0.0\"` field deep in its payload (response\nenvelope, captured config blob, metadata wrapper) would silently bypass\nPII scanning. The YAML markers were already anchored via `(?m)^` to\ncolumn 0, which in valid YAML means a root-level key.\n\nTighten the JSON regexes to `\\A\\s*\\{\\s*\"openapi\"\\s*:\\s*\"[23]\\.`,\nrequiring `openapi` (or `swagger`) as the first key after the opening\nbrace. JSON specs put the version key at root by convention; the strict\nanchor matches the YAML behavior and forecloses the nested-key bypass.\n\nAdds three regex-level cases (nested openapi inside `user_data`, info\nbefore openapi, nested swagger) and an end-to-end FindPII regression\ntest that builds a captured.json with a nested openapi field plus real\nPII and asserts the file still scans.\n\n* fix(cli): anchor YAML vendor-spec markers to document start\n\nGreptile's second pass flagged the symmetric YAML gap. The previous\n`(?m)^openapi\\s*:` regex fired on any column-0 occurrence within the\n8 KB head probe — so a research-notes YAML under .manuscripts/ that\nlisted real PII in earlier root-level keys and a later `openapi: 3.0.0`\nkey would silently bypass scanning. The JSON anchor (`\\A\\s*\\{\\s*\"openapi\"`)\nforecloses the same shape; YAML needed parallel treatment.\n\nAnchor both YAML markers to document start with an explicit allowed\nlead-in (optional YAML directives `%...`, document marker `---`,\ncomment lines, blank lines). Any other content before `openapi`/`swagger`\nat column 0 rejects the exemption.\n\nAdds:\n- four regex-level cases: yaml-pii-before-openapi (negative), yaml-with-\n  document-marker (positive), yaml-with-comment-prefix (positive),\n  yaml-with-directive-and-marker (positive), yaml-with-blank-lead\n  (positive), yaml-swagger-after-meta (negative).\n- a YAMLPIIBeforeOpenAPI FindPII regression that mirrors the JSON one.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"9d3050b5","date":"2026-05-15 23:31:01 -0700","author":"Trevin Chow","subject":"fix(cli): always emit boolean body fields, no zero-guard (#1494)","body":"* fix(cli): always emit boolean body fields, no zero-guard\n\nBooleans rendered by renderBodyMap previously sat behind an `if body* != false`\nguard that omitted the field whenever the flag was unset. Many APIs server-default\nboolean body fields to true (Freshservice notes private, Stripe automatic_*\nenabled, etc.), so the omission path silently inverts the user's intent: a flag\ndefaulting to public posts as private.\n\nBoolean params now always emit, mirroring the established `p.Type == \"boolean\"\n|| p.Type == \"bool\"` pattern elsewhere in the generator (internal YAML specs use\n\"boolean\", the OpenAPI parser normalizes to \"bool\"). String and integer scalars\nkeep their zero-guard, since their zero values (\"\" and 0) more commonly map to\n\"unset\" on real APIs.\n\nCloses #1298\n\n* fix(cli): gate boolean body emission on cmd.Flags().Changed, not zero-guard\n\nGreptile review on #1494 surfaced that \"always emit booleans\" trades the\nuser-false-drop bug for a worse one on PATCH endpoints: every untouched\nboolean flag would silently send field=false on the wire and overwrite\nserver state. A user running `update-project --title \"X\"` would\nunintentionally revert any previously-completed task to incomplete.\n\nSwitching to `cmd.Flags().Changed(flag)` distinguishes \"user explicitly\nset false\" from \"user did not touch the flag\" and is correct for POST,\nPUT, and PATCH. The Changed gate also threads cleanly through the\nnested-object recursion: an untouched boolean leaf adds nothing to\nthe inner map, so `len(nestedMap) > 0` stays false and the parent\nobject key is not emitted.\n\nTests cover both type-form spellings (`boolean` from internal YAML\nspecs, `bool` from the OpenAPI parser) and the nested-object case.\n\nRefs #1298\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"96fca081","date":"2026-05-15 22:51:11 -0700","author":"Trevin Chow","subject":"feat(cli): expose PRINTING_PRESS_DOGFOOD env signal for long-running commands (#1493)","body":"* feat(cli): expose PRINTING_PRESS_DOGFOOD env signal for long-running commands\n\nThe live-dogfood matrix applies a flat 30s per-command timeout. Long-running\nnovel commands (full sync loops, content crawlers, bulk archive walks) trip\nthe timeout on their real happy path and the matrix verdict flips to FAIL\neven when the command itself is healthy.\n\nThis adds a dogfood env signal that mirrors the existing PRINTING_PRESS_VERIFY\npattern: the runner sets PRINTING_PRESS_DOGFOOD=1 on every subprocess, and\ncliutil.IsDogfoodEnv() is emitted into every printed CLI so commands can\nshort-circuit to a bounded code path (paginate once, smaller --limit) and\nfit inside the timeout.\n\nUnlike IsVerifyEnv, this does not mean \"don't hit the network\" -- dogfood\nis a real-API matrix. The helper docs and the new AGENTS.md / SKILL.md\nsections call out the distinction so authors don't substitute mocked data\nfor real calls.\n\nIncludes a test that asserts the runner-side const matches the template-side\nemitted helper so a future rename on either side doesn't silently break\nevery IsDogfoodEnv() short-circuit.\n\nCloses #1232\n\n* test(cli): unset PRINTING_PRESS_DOGFOOD before env-propagation assertion\n\nWithout the unset, a CI runner that already has PRINTING_PRESS_DOGFOOD=1\nin its environment would pass the assertion via inheritance through\napplyDefaultSubprocessEnv -> os.Environ() regardless of whether the\nrunner's own append line is present. t.Setenv with empty string forces\nthe test to prove the append line is what gets the var into the\nsubprocess.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"6fc98d75","date":"2026-05-15 21:36:38 -0700","author":"Matt Van Horn","subject":"chore(main): release 4.8.0 (#1491)","body":""},{"hash":"f8d3ba80","date":"2026-05-15 20:24:16 -0700","author":"Trevin Chow","subject":"fix(cli): short-circuit sync on dry-run sentinel response (#1471)","body":"* fix(cli): short-circuit sync on dry-run sentinel response\n\nclient.dryRun returns `{\"dry_run\": true}` instead of a real response\nwhen --dry-run is set. The sync template fell through to\nupsertSingleObject against the sentinel, which failed with\n\"missing id for <resource>\" because the sentinel has no items and no id.\nEvery CLI shipped with a `sync` quickstart blocked shipcheck because\nvalidate-narrative --full-examples auto-appends --dry-run.\n\nAdd isDryRunResponse(data) helper and check it right after c.Get in\nsyncResource. On match, emit a structured sync_dryrun event and return\na zero-count syncResult so the dry-run preview path exits cleanly.\n\nThree golden fixtures updated (generate-golden-api,\ngenerate-sync-walker-api, generate-tier-routing-api) for the new\nshort-circuit + helper emission.\n\nCloses #1382\n\n* fix(cli): require exact one-key dry_run sentinel in sync short-circuit\n\nGreptile flagged that the previous isDryRunResponse helper unmarshalled\ninto a single-field struct, which would silently match any real API\nresponse containing a top-level dry_run field alongside other data —\nzeroing the sync count without an error. Tighten the check to require\nexactly one key in the envelope (dry_run) with a boolean true value.\nThree goldens regenerated for the helper body change.\n\nAlso drop a ticket-number reference from a test comment per the\nproject's no-ticket-numbers-in-code-comments rule.\n\nRefs #1382\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"5f43de50","date":"2026-05-15 21:44:53 -0500","author":"Cathryn Lavery","subject":"feat(cli): add ElevenLabs generation support (#1307)","body":"* feat(cli): add ElevenLabs generation support\n\n* fix(cli): share catalog metadata helpers\n\n* fix(cli): scope headerOverrides to branches that use it in promoted commands\n\nThe promoted-command template declared `headerOverrides` at the top of\nthe handler when UsesBinaryResponse was set, but the paginated branch\n(both HasStore and non-HasStore) passes nil to resolvePaginatedRead /\npaginatedGet and never references the variable. The same was true for\nthe HasStore + GET branch that calls resolveRead. An endpoint with both\nUsesBinaryResponse=true and pagination (or HasStore-GET) generated Go\nthat failed to compile with \"headerOverrides declared and not used\".\n\nMove the declaration into each branch that actually consumes it (live\nGET, DELETE, multipart, form, and JSON-body POST/PUT/PATCH). Regression\ntest pins a paginated binary GET and runs `go build` on the output.\n\nReported by Greptile on #1307.\n\n* fix(cli): thread binary-response header through promoted pagination and store paths\n\nThe promoted-command template called paginatedGet, resolvePaginatedRead,\nand resolveRead with nil headers regardless of UsesBinaryResponse. When\nthe live API call dispatched (cache miss for store-backed reads, or any\npaginated binary GET), the BinaryResponseHeader sentinel never reached\nthe client, so the runtime ran sanitizeJSONResponse on raw binary bytes\nand sent Accept: application/json — corrupting audio responses and\nmaking strict APIs return JSON variants of binary endpoints.\n\nMirror the pattern already used in command_endpoint.go.tmpl: hoist\nheaderOverrides to the top of the handler when UsesBinaryResponse is\nset, then thread headerOverrides (else nil) through every helper call\nthat accepts headers. Drops the per-branch headerOverrides declarations\nthe previous compile-fix introduced — all consumers now share one.\n\nTwo regression tests pin the wiring: the paginated path asserts\nheaderOverrides is passed to paginatedGet (forcing no-store via an\nexplicit VisionTemplateSet so IsZero() skips re-profiling), and the\nstore-backed path asserts resolveRead receives headerOverrides instead\nof nil.\n\nReported by Greptile on #1307.\n\n* chore(cli): refresh GraphQL shared-endpoint goldens after main merge\n\nMergify's update_method=merge brought main into this branch, which\nincludes the new PostWithParams helper. The GraphQL shared-endpoint\ngenerator now threads a params map and calls PostWithParams instead\nof Post for both list and get operations. Regenerated goldens to\nreflect the merged behavior; no template change in this commit.\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>\nCo-authored-by: Trevin Chow <trevin@trevinchow.com>"},{"hash":"55f5b26b","date":"2026-05-15 18:04:04 -0700","author":"Trevin Chow","subject":"fix(cli): expand pm_stale timestamp-field list for non-updated_at APIs (#1461)","body":"* fix(cli): expand pm_stale timestamp-field list for non-updated_at APIs\n\nGenerated `internal/cli/pm_stale.go` queried only `$.updatedAt` and\n`$.updated_at`, so any API using a different modification-timestamp\nfield returned zero stale items even when the local store had\nthousands of stale rows. Confirmed with Asana (modified_at); Notion\n(last_edited_time), Stripe (updated), and other variants share the\nsame shape.\n\nCentralise the field list in a single `staleTimestampFields` slice and\nbuild the WHERE/ORDER BY clauses from it, so a future addition lands in\none place. Cover the historical pair plus modified_at, last_modified,\nlast_edited_time, modifiedAt, lastEditedTime, updated, last_updated.\n\nA regression test in generator_test pins the field list and the\nbuild-from-constant approach.\n\nCloses #1391\n\n* fix(cli): COALESCE pm_stale ORDER BY across timestamp fields\n\nGreptile flagged the multi-column ORDER BY: SQLite ASC sorts NULLs\nfirst, so a row whose only timestamp is e.g. modified_at would rank\nabove rows that populate updatedAt because the leading ORDER BY column\nsees NULL on the first row but a real value on the second. The fix:\nuse COALESCE so each row sorts by its first non-null timestamp,\npreserving the oldest-first guarantee across mixed-API stores.\n\nAlso extend the test field list to include updatedDate (was already\nin the template but missing from the assertion) and pin the\nCOALESCE-based ORDER BY shape so a future refactor can't silently\nregress to a multi-column sort.\n\nRefs #1391\n\n* fix(cli): typeof-gate pm_stale WHERE for integer-typed timestamps\n\nGreptile flagged that the WHERE predicate compared json_extract output\nto an RFC 3339 string cutoff. SQLite's type-class ordering makes any\nJSON number unconditionally less than any text value, so APIs that\nstore modification timestamps as Unix epoch seconds (Stripe's\n`updated`) would have every row falsely satisfy the predicate.\n\nGate each per-field comparison with typeof():\n- text values compare against an RFC 3339 cutoff\n- integer/real values compare against a Unix epoch cutoff\n\nThe Go-side scan loop now type-switches on the parsed JSON value so\ndays_since is computed correctly from both string and numeric inputs.\n\nRefs #1391\n\n---------\n\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"3bb6d37b","date":"2026-05-15 20:51:33 -0400","author":"tallyberner","subject":"fix(cli): doctor preserves configured User-Agent when API uses UA as auth credential (#1361)","body":"* fix(cli): doctor preserves configured User-Agent when API uses UA as auth credential\n\nWhen an API spec declares Auth.Header == \"User-Agent\" with Auth.In ==\n\"header\" (e.g. weather.gov's userAgent securityScheme), the doctor\ntemplate's credential-probe block emitted two consecutive assignments\nto the same map key:\n\n    authHeaders[\"User-Agent\"] = authHeader              // configured UA\n    authHeaders[\"User-Agent\"] = \"<name>-pp-cli\"       // hardcoded fallback\n\nThe fallback unconditionally clobbered the operator's configured UA, so\nthe probe tested the wrong identity. For NWS, the credential probe hit\napi.weather.gov with the hardcoded \"<name>-pp-cli\" instead of the\noperator's contact-info UA, defeating the entire point of NWS's UA-based\nauth.\n\nRoot cause: doctor.go.tmpl gated the fallback only on\n.UsesBrowserManagedUserAgent and .HasRequiredHeader \"User-Agent\". It\ndidn't notice that the line above had already set\nauthHeaders[\"User-Agent\"] = authHeader when the auth header itself is\nUser-Agent.\n\nFix: add a third skip condition. Compute authUsesUA = Auth.In is\n\"header\" (or empty default) AND lower(Auth.Header) == \"user-agent\";\nskip the fallback when true.\n\nTests pin both directions: the UA-as-auth case must NOT emit the\nhardcoded fallback, and the bearer-auth case (Auth.Header is\nAuthorization) must STILL emit it so the probe identifies itself when\nUser-Agent isn't the credential.\n\nSurfaced by pp-nws-storm Codex review 2026-05-13.\n\n* Update internal/generator/doctor_auth_status_test.go\n\nCo-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>\n\n* fix(cli): gofmt internal/generator/doctor_auth_status_test.go\n\ngo-lint was failing on a misaligned struct literal in\nTestDoctorPreservesUserAgentWhenUsedAsAPIKey. gofmt -w aligns the\nType/Header fields. No behavior change.\n\n---------\n\nCo-authored-by: Dior Vass <dior@diorconstruction.com>\nCo-authored-by: Trevin Chow <trevin@trevinchow.com>\nCo-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>\nCo-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>"},{"hash":"2d0ad5ae","date":"2026-05-15 17:36:48 -0700","author":"Trevin Chow","subject":"fix(ci): switch queue update_method to merge so fork PRs can queue (#1492)","body":"Fork PRs sit BLOCKED in this repo despite green CI because Mergify's\ndefault bot account can't push rebase commits to a fork branch in a\nway that triggers GitHub Actions. `@mergifyio queue` exposes this with\n\"GitHub permissions prevent queueing\".\n\nThe historical workaround was `update_bot_account: <maintainer>` so\nthe rebase ran as a real user, but Mergify deprecated the\n`update_method=rebase` + `update_bot_account` combination for fork\nPRs on 2026-04-21 (removal 2026-07-01). The published migration path\nis `update_method=merge`.\n\nWith `merge`, Mergify merges main INTO the PR branch (creating a\nmerge commit) instead of rebasing. mergify[bot] can do this on fork\nbranches without impersonation, and the merge commit triggers\nworkflows. `merge_method: squash` is unchanged, so the final commit\non main is still a single squashed commit — on-main history is\nidentical to the rebase-based flow.\n\nReaches PRs #865 (mschreib28) and #1361 (tallyberner) which were\nboth stuck on this exact path."},{"hash":"5460c32a","date":"2026-05-15 17:32:20 -0700","author":"Trevin Chow","subject":"fix(cli): probe nested path-param leaves so verify catches `args[]` index bugs (#1434)","body":"* fix(cli): probe nested path-param leaves so verify catches `args[]` index bugs\n\nverify's matrix only invokes top-level commands. Nested leaves like\n`tags contacts list-for-tag-id-using-get <tagId>` were never exercised\nwith positionals, so a generator codegen bug that mis-indexed\n`args[]` for path-param substitution (issue #1199, fixed by #1211)\nshipped silently with verify reporting 100% pass on three published\nCLIs (keap, supabase, learndash).\n\nAdd a positional-binding probe that recursively walks the cobra tree\nvia --help, finds leaves whose Usage line carries `<placeholder>`\ntokens, and runs each with `--dry-run <synthetic-positional>`. A leaf\nis flagged only when the binary exits non-zero AND the output contains\nthe \"is required\" sentinel — unrelated failures (auth, transient,\nintentional non-zero) are not treated as probe failures so\nalready-correct CLIs continue to pass. Top-level leaves (depth 1) are\nskipped because the existing matrix already exercises them through\ninferPositionalArgs; the probe starts at depth 2 to avoid\ndouble-counting.\n\nFailed probes are critical: they're surfaced as a new\nPathParamProbes list on VerifyReport (omitempty) and folded into the\nexisting Total / Failed / Critical counts in finalizeVerifyReport, so\na single failing probe blocks the PASS verdict.\n\nCloses #1198.\n\n* docs(cli): scrub em-dashes and ticket numbers from path-param probe comments\n\nAGENTS.md \"Code & Comment Hygiene\": no em-dashes or ticket numbers in\ncode comments. Drops the bare-ticket-reference shape in favor of\nself-contained prose so the comments hold up if the referenced issue is\nrenumbered or migrated.\n\n* fix(cli): probe only true leaves; replace Args with Positionals on probe result\n\nAddresses Greptile P2 findings on #1434:\n\n1. Parent/group commands whose Usage line carried a `<placeholder>`\n   token were being added to the probe set even when they had\n   children. The synthetic value would land as an \"unknown command\",\n   exit non-zero without matching the \"is required\" sentinel, and\n   silently pass — inflating Total / Passed without testing anything\n   meaningful. Probes now require the node to have no children\n   (`parseHelpCommands(helpOut)` returns empty). The walker still\n   recurses into every child regardless, so true leaves underneath a\n   placeholder-bearing parent are still discovered.\n\n2. PathParamProbeResult.Args duplicated the command path that\n   Command already carries, leaving JSON consumers to strip the\n   leading words to recover the injected positionals. Replaced with\n   Positionals, which carries only the synthetic values one per\n   placeholder; Command + Positionals together describe the full\n   invocation without redundant prefix."},{"hash":"d7ccd850","date":"2026-05-15 17:12:27 -0700","author":"Trevin Chow","subject":"fix(cli): use Chrome User-Agent for synthetic/cookie/composed/session_handshake CLIs (#1479)","body":"* fix(cli): use Chrome User-Agent for synthetic/cookie/composed/session_handshake CLIs\n\nGenerated CLIs default to `<cli>-pp-cli/<version>` in the User-Agent\nheader. For documented JSON APIs this is fine — vendor support uses\nthe identifier. But for browser-sniffed (Kind: synthetic) CLIs and any\nauth.type in {cookie, composed, session_handshake}, the script-shaped\nUA is exactly what WAFs (Wordfence, Imperva, Akamai bot-mode,\nDataDome, Cloudflare bot-fight) bucket as bot traffic and reject with\n5xx, 403, or a challenge. Confirmed on bbquality.nl WP/Wordfence and\ndocumented as a hand-fixed pattern in the picnic-pp-cli source.\n\nAdd UsesBrowserLikeUserAgent() on APISpec that flips true for the\nabove kinds. Wire client.go.tmpl and auth_browser.go.tmpl to emit a\nChrome UA on that branch while keeping the script-flavored default for\ndocumented APIs. UsesBrowserManagedUserAgent (chrome/h3 transports)\nstill short-circuits both paths entirely.\n\nCloses #1293\n\n* test(cli): extend UA template coverage and drop fragile nil dispatch\n\nGreptile flagged two valid test gaps in the previous commit:\n\n- TestClientTemplateBrowserLikeUserAgent only read client.go.tmpl, so\n  an accidental revert of the parallel auth_browser.go.tmpl change\n  would go undetected (auth-verify silently reverts to script UA).\n  Rename to TestBrowserLikeUserAgentTemplates and loop over both\n  template files with the same three pin assertions.\n\n- TestUsesBrowserLikeUserAgent dispatched the nil-receiver case via\n  a literal name-string comparison against tc.name. Renaming the\n  case would silently fall through to UsesBrowserLikeUserAgent on a\n  zero-value APISpec{} (which also returns false) and leave the nil\n  guard untested. Switch the table to *APISpec so the nil path is a\n  value, not a string match.\n\nRefs #1293"},{"hash":"e8eb4e30","date":"2026-05-15 14:33:41 -0700","author":"Matt Van Horn","subject":"chore(main): release 4.7.0 (#1377)","body":""},{"hash":"ede5c350","date":"2026-05-15 13:58:42 -0700","author":"Trevin Chow","subject":"fix(cli): honor explicit \"mcp:read-only\": \"false\" in tools-audit (#1485)","body":"* fix(cli): honor explicit \"mcp:read-only\": \"false\" in tools-audit\n\ninspectAnnotations collapsed \"true\" and absent into hasReadOnly=false,\nso kindMissingReadOnly fired on commands whose authors had correctly\nopted out by writing \"false\" (e.g., `watch report` — name matches the\nread-shape heuristic but the impl actually fetches+upserts).\n\nRename the signal to hasExplicitReadOnly: true when the annotation\nkey is present with ANY value, false only when genuinely absent. The\naudit's job is to flag unannotated commands, not enforce that every\nread-shaped name be read-only.\n\nCloses #891\n\n* test(cli): move hasExplicitReadOnly doc to right field; trim redundant case\n\nGreptile noted two small issues in the previous commit:\n\n- The new hasExplicitReadOnly doc comment sat above `short` due to a\n  struct-field-order accident. Move it so go doc/IDE hover land on the\n  field the comment describes.\n\n- The auditCommandFields tests had two structurally identical cases\n  (explicit-true and explicit-false both bottom out at\n  hasExplicitReadOnly=true by the time they reach this function, so\n  the function can't distinguish them — the AST-level differentiation\n  lives in TestInspectAnnotationsExplicitReadOnlyFalse, which already\n  has all three input shapes). Collapse to a single \"annotation present\"\n  case at the auditCommandFields layer.\n\nRefs #891"},{"hash":"f354bc7a","date":"2026-05-15 13:45:45 -0700","author":"Trevin Chow","subject":"docs(skills): document hierarchical resource_type in store-query skeleton (#1484)","body":"* docs(skills): document hierarchical resource_type in store-query skeleton\n\nThe store-query RunE skeleton in skills/printing-press/SKILL.md used\nSELECT id, data FROM <table> WHERE ... with no guidance on the\nresources-table column model. Agents who followed the skeleton against\nhierarchical resources synced from /<parents>/{id}/<children> filtered\nby the bare child name (resource_type='tasks') and got zero rows\nbecause the press stores those as resource_type='<parent>_<child>'\n(projects_tasks for Asana, repos_issues for GitHub, etc).\n\nReplace the placeholder query with a SELECT FROM resources filtered\nvia IN ('<resource>', '<parent>_<resource>'), and add a paragraph\nabove the code block explaining the convention so the next agent\ndoesn't have to debug a zero-rows result. Mention the typed FTS/upsert\ntables as the fast path for flat-only resources, and point at\n`printing-press dogfood --json` for confirming the actual resource_type\ndistribution.\n\nCloses #1395\n\n* docs(skills): use <resource> consistently in store-query intro\n\nGreptile noted the intro paragraph used <child> as the placeholder\nname while the query directly below used <resource> for the same\nsubstitution target, which could confuse an agent following the\nskeleton. Normalize both to <resource>.\n\nRefs #1395"},{"hash":"77b91be6","date":"2026-05-15 13:42:04 -0700","author":"Trevin Chow","subject":"docs(cli): canonicalize README install block + cross-repo sweep dependency note (#1464)","body":"* docs(templates): canonicalize README install block + cross-repo sweep dependency note\n\nMake `internal/generator/templates/readme.md.tmpl` produce the\nsame canonical install block that the published library now\ncarries. Fresh prints from this generator land canonical on day\none; the published library's `tools/sweep-canonical/` handles\nretrofitting older entries.\n\n## Install block\n\n- Headline names the agent set the bundled skill install reaches:\n  Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, plus a\n  link to the upstream `skills` CLI for the full list.\n- Documents `--cli-only`, `--skill-only`, and `--agent` (repeatable,\n  single and multi examples) as first-class options below the\n  default command. The previous template only showed `--cli-only`;\n  `--skill-only` and `--agent` existed in the installer but weren't\n  surfaced to readers.\n\n## Section reordering\n\n- `## Use with Claude Desktop` moves from near the bottom of the\n  README up to live right after `## Install for OpenClaw`. All\n  alternate install paths are now grouped at the top of the README:\n  Install → Install for Hermes → Install for OpenClaw → Use with\n  Claude Desktop (when present).\n- `## Use with Claude Code` is removed entirely. Its `npx skills\n  add` install command is now redundant with the canonical\n  `## Install` block (which documents `--skill-only` for the same\n  effect). Its MCP `<details>` subsection lived in an unstructured\n  spot and inconsistent across CLIs; if Claude Code MCP-registration\n  guidance turns out to be load-bearing, we can add it back as a\n  dedicated section rather than buried inside a Claude-Code-only\n  section.\n\n## Cross-repo dependency note\n\nAdds a \"Cross-repo dependency: published-library sweep tool\"\nsection to AGENTS.md that captures the relationship: template\nchanges that shift canonical published-library shape must be\nmirrored in `tools/sweep-canonical/main.go` in\n`mvanhorn/printing-press-library` so existing entries can be\nretrofitted, or a tracking issue must be filed before the template\nPR lands. Without that, fresh prints get the new shape while\nexisting entries silently drift.\n\n## Tests\n\n- 878 generator tests pass.\n- Golden fixtures updated for 3 cases whose README.md output shifts\n  (generate-golden-api, generate-golden-api-rich-auth,\n  generate-public-param-names) — diffs verified to match the\n  intended template change (new install subsections + Claude Desktop\n  moved up + Claude Code section deleted).\n- Removed TestGeneratedOutput_READMEBearerTokenMCPSetup, which\n  asserted on a `claude mcp add ...` command rendered inside the\n  now-deleted `## Use with Claude Code` section. The MCP-binary\n  registration path lives in the Claude Desktop section's manual\n  JSON config `<details>`; if a separate Claude Code MCP test is\n  needed later it should target whatever section we add for that.\n\nThe matching library sweep landed in mvanhorn/printing-press-library#584.\n\n* fix: address Greptile feedback (#1464)\n\n1. Drop the extra blank line before `### Without Node` in the\n   rendered README. The new `--agent` subsection's trailing blank\n   plus the un-trimmed `{{ if .Category}}` directive's preserved\n   newline produced two consecutive blank lines. Removing the\n   template-side blank line restores single-blank-line separation.\n\n2. Restore bearer_token README coverage. Removing\n   `TestGeneratedOutput_READMEBearerTokenMCPSetup` correctly\n   followed the deletion of `## Use with Claude Code`, but left\n   the bearer_token branches in the relocated `## Use with Claude\n   Desktop` section untested. New\n   `TestGeneratedOutput_READMEBearerTokenClaudeDesktop` exercises\n   the canonical-env-var-present branch (mirrors the original\n   test's spec shape) and asserts on the step-3 prompt naming the\n   env var, the env var in the Manual JSON config block, and the\n   absence of the canonical-env-var-absent preamble.\n\n3. Update goldens for the blank-line fix (3 README fixtures)."},{"hash":"a9ffa08f","date":"2026-05-15 13:29:46 -0700","author":"Trevin Chow","subject":"fix(cli): research-dir state.json api_name overrides info.title-derived name (#1476)","body":"When `generate --research-dir <dir>` ran with a state.json that recorded\n`api_name: canvas`, the generator still derived the CLI/module name\nfrom `cleanSpecName(info.title)` (\"Canvas LMS API\" -> `canvas-lms`).\nThe mismatch made `publish validate --help/--version` fail because the\nmanifest's `cli_name: canvas-pp-cli` pointed at a `cmd/canvas-pp-cli/`\ndirectory that didn't exist; only `cmd/canvas-lms-pp-cli/` did.\n\nAdd `pipeline.LoadAPINameFromResearchDir(dir)` that reads\n`<dir>/state.json` and returns the recorded `api_name`. Wire it into the\ngenerate command's single-spec branch as an implicit `--name` override\nthat yields to an explicit `--name` flag. Behavior without\n`--research-dir` is unchanged.\n\nCloses #1375"},{"hash":"272d8e83","date":"2026-05-15 13:14:56 -0700","author":"Trevin Chow","subject":"fix(cli): preserve OpenAPI param casing in detected pagination (#1460)","body":"detectPagination built a case-insensitive lookup map but then stored the\ncandidate lookup key (always lowercase) as Pagination.LimitParam /\nCursorParam. Specs declaring camelCase params like pageSize, pageToken,\nor maxResults shipped a sync that sent the lowercased form, which\nGoogle APIs reject with HTTP 400 (\"Unknown name 'pagesize'\").\n\nSwitch the lookup to a lowercase->original map and store the spec's\noriginal casing. Test matrix covers pageSize/page_size/limit/maxResults\nplus the cursor variants pageToken/page_token/after.\n\nCloses #1353"},{"hash":"01c816da","date":"2026-05-15 13:01:56 -0700","author":"Trevin Chow","subject":"fix(cli): pick gid/sid/uid before name in PK heuristic (#1465)","body":"* fix(cli): pick gid/sid/uid before name in resolveIDFieldFromResponseSchema\n\nAsana and Twilio key every resource on a vendor-specific identifier\n(gid for Asana, sid for Twilio). The PK heuristic in\nresolveIDFieldFromResponseSchema previously checked for id (tier 2),\nthen resource-prefixed keys (tier 3), then `name` (tier 4) — so APIs\nwithout bare `id` got `name` written into resourceIDFieldOverrides.\nGenerated CLIs then upserted on display names and downstream paths\nlike /workspaces/<workspace>/users got a name where the API expected\na gid, causing HTTP 400.\n\nInsert a new tier 3.5 between the resource-prefixed lookup and the\n`name` fallback that scans for gid, sid, uid, uuid, guid. Also reorder\nthe runtime genericIDFieldFallbacks list (in sync.go.tmpl, store.go.tmpl,\nand store_upsert_batch_test.go.tmpl) so the vendor names take\nprecedence over `name` for resources that never received a templated\noverride.\n\nGolden fixtures updated for the three sites that emit the fallback\nlist (generate-golden-api, generate-sync-walker-api,\ngenerate-tier-routing-api).\n\nCloses #1394\n\n* docs(cli): update PK fallback doc comment and add guid test case\n\nGreptile noted two small gaps in the previous commit:\n- resolveIDFieldFromResponseSchema's function-level doc comment still\n  described the pre-3.5 chain without mentioning the new vendor-identifier\n  tier. Update to list gid/sid/uid/uuid/guid alongside the existing tiers.\n- The table-driven test had no dedicated `guid` case, leaving the fifth\n  entry of the tier 3.5 loop without coverage. Add it.\n\nRefs #1394"},{"hash":"72adfefd","date":"2026-05-15 15:50:20 -0400","author":"Kyle Kirkland","subject":"fix(cli): drop unused client import from param-less endpoint command files (#911)","body":"* fix(cli): drop unused client import from param-less endpoint command files\n\nThe per-endpoint command template emits internal/client whenever the\nspec is GraphQL-shaped and the endpoint sits at /graphql with name\nlist or get. The corresponding `client.X` reference inside the rendered\nbody only lands in the GET branch, so any GraphQL spec recording its\nlist/get endpoints with method POST (the wire-correct method, since\nGraphQL queries POST to /graphql) ships an unused import. Go's strict\nunused-import rule fires and `go build` fails on every such CLI.\n\nA post-render fixup over command_endpoint.go.tmpl output drops the\n`<module>/internal/client` import line when the rendered body never\nreferences the `client` package as a qualifier. Endpoints that DO\nreference `client.X` (current GraphQL GET branches) keep the import.\n\nCloses #908\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>\n\n* fix: comments\n\nCo-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>\n\n* test(cli): add golden fixture for GraphQL shared-endpoint client-import pruning\n\nLocks the import-block shape for the POST /graphql list/get branch so\nfuture template edits cannot silently re-introduce the unused\ninternal/client import that this PR's pruneUnusedClientImport addresses.\nTestEndpointCommandBuildsPostFixup catches compile-time regressions, but\nwithout a golden the rendered import block is unwitnessed and a future\nrefactor could re-add the line under a different code path that still\ncompiles for some unrelated reason.\n\nAddresses the second Greptile review comment on #911 per the AGENTS.md\nrule: \"When adding a new deterministic CLI behavior or generated\nartifact contract, explicitly decide whether the golden suite needs a\nnew or expanded fixture.\"\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>\n\n* test(cli): refresh generate-graphql-shared-endpoint goldens against current main\n\nThe fixture introduced by this PR was captured before main added:\n- gofmt of rendered .go templates in normalizeRendered (alters struct\n  field alignment, multi-line struct literals)\n- expanded terminal-detection guard for table output that takes --csv /\n  --plain / --quiet into account\n\nBehavior of the import-prune change in this PR is unchanged — the\nfixture diff is purely the downstream of main's progression.\n\n---------\n\nCo-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>\nCo-authored-by: Trevin Chow <trevin@trevinchow.com>\nCo-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>"},{"hash":"35e515c1","date":"2026-05-15 12:37:22 -0700","author":"Trevin Chow","subject":"fix(cli): query generic resources_fts from search empty-type branch (#1462)","body":"* fix(cli): query generic resources_fts from search empty-type branch\n\nThe empty-type branch of search.go iterated only typed FTS tables, so\nrows indexed in resources_fts but not in any typed FTS table returned\nzero from <cli> search \"<query>\" (no --type). Confirmed on Asana CLI\nwhere direct SELECT FROM resources_fts MATCH ... returned 249 hits but\nsearch \"\" reported zero.\n\nAppend a db.Search(query, limit) call to the existing seen-map dedup\nloop so the generic index is queried alongside the typed Search<X>\nhelpers. Test pins both the call and the contextual error message.\n\nCloses #1390\n\n* test(cli): anchor search-template default: slice on indentation\n\nGreptile noted the strings.Index search for \"default:\" inside the\nempty-type branch is fragile: a future comment containing \"default:\"\nwould shift the slice and silently under-test the assertions. Anchor\non \"\\n\\t\\t\\tdefault:\" so the boundary lands on the actual switch arm.\n\nRefs #1390"},{"hash":"bb5e2def","date":"2026-05-15 12:24:49 -0700","author":"Trevin Chow","subject":"fix(cli): validate &&-chained narrative recipes segment-by-segment (#1447)","body":"* fix(cli): validate &&-chained narrative recipes segment-by-segment\n\nvalidate-narrative previously fed the whole recipe string to\nshellargs.Split, so `cli sync && cli expiring --within 60d` ran with\n`&&` as a positional arg and failed with `unknown flag: --within`.\n\nsplitShellChain walks the command with shellargs-compatible quote\nhandling and returns top-level segments separated by `&&`, `||`, or\n`;`. classify then validates each segment independently. Top-level `|`\nrecipes classify as Unsupported with `pipe-skipped` rather than running.\n\nCloses #1271\n\n* fix(cli): pass trimmed segment to classify in single-segment fast path\n\nGreptile flagged that splitShellChain trims trailing operators\n(e.g. \"stub sync && \" → [\"stub sync\"]) but classify was still handing\nthe original command to classifySegment. In FullExamples mode that\nleaked the operator into shellargs.Split's token stream, producing a\nspurious StatusExampleFailed.\n\nPass segments[0] when present, fall back to the original command for\nthe zero-segment case (whitespace-only or operator-only inputs).\nTighten the test comparison to reflect.DeepEqual now that the suite\nexercises pipe-bearing inputs.\n\nRefs #1271"},{"hash":"a1098f15","date":"2026-05-15 11:50:20 -0700","author":"Trevin Chow","subject":"fix(cli): populate printer + run_id in generated .printing-press.json (#1442)","body":"* fix(cli): populate printer + run_id in generated .printing-press.json\n\nStandalone `printing-press generate` left both fields empty by default, so\npublish-validate's manifest contract failed on every freshly generated CLI\nthat polish hadn't hand-patched.\n\n- resolvePrinterForNew now falls back to `gh api user --jq .login` when\n  `git config github.user` is unset, covering machines where the user has\n  authenticated gh without setting the git config.\n- WriteManifestForGenerate auto-fills an empty RunID with a fresh\n  YYYYMMDD-HHMMSS so the manifest always has a non-empty run_id. Phase 5\n  callers still need the research-dir-derived ID and root.go keeps its\n  --research-dir warning for that case.\n- Golden harness normalizes run_id alongside generated_at; the five\n  affected fixtures regain a stable shape.\n\nCloses #1348\n\n* fix(cli): harden printer fallback against gh null output and unisolated PATH\n\nAddress Greptile P2 review on #1442:\n\n- Reject the literal string \"null\" from `gh api user --jq .login` (jq emits\n  this when the API response lacks `.login` or returns JSON null); without\n  the filter the printer field would be set to \"null\" and still fail\n  publish-validate the same way an empty printer does.\n- stubBinaryDir now prepends to PATH instead of replacing it entirely so\n  TestGenerateLeavesPrinterEmptyWhenGitHandleUnset (which calls the broader\n  gen.Generate(), not just resolvePrinterForNew) keeps non-stubbed binaries\n  reachable.\n- Add TestResolvePrinterForNewRejectsGhNullOutput."},{"hash":"9d70aee4","date":"2026-05-15 11:33:54 -0700","author":"Trevin Chow","subject":"chore(ci): add CODEOWNERS for workflows and CODEOWNERS itself (#1472)","body":"* chore(ci): add CODEOWNERS to gate edits to workflows and CODEOWNERS itself\n\nDefense in depth alongside the org-level \"Approve fork PR workflows for\nfirst-time contributors who are new to GitHub\" setting. Workflow files\nhave access to repo secrets at runtime; CODEOWNERS controls who can edit\nthe gating rules themselves. Both must require an explicit maintainer\nreview before merging.\n\nLists @tmchow and @mvanhorn so bus factor is 2 and a single absence\ndoesn't block workflow changes.\n\n* chore(ci): extend CODEOWNERS to scripts/ and .github/scripts/\n\nGreptile P1 flagged the gap: workflows run `bash .github/scripts/validate-skill-docs.sh`\nand `scripts/golden.sh verify` in steps that have secrets in scope, so an\nedit to either script directory has the same exfiltration potential as\nediting the workflow YAML. Add both to the gate."},{"hash":"bd4dc641","date":"2026-05-15 11:22:22 -0700","author":"Trevin Chow","subject":"fix(cli): kebab-case SKILL.md and README endpoint examples (#1445)","body":"* fix(cli): kebab-case SKILL.md and README endpoint examples\n\nSKILL.md and README \"Command Reference\" lines emitted the raw endpoint\nkey from the spec, so a spec with `dns.get_hosts` advertised\n`<cli> dns get_hosts` while the actual Cobra subcommand was\n`dns get-hosts` (the `command_endpoint.go.tmpl` `Use:` field already\nroutes through `{{kebab .EndpointName}}`). verify-skill rejected those\ndocs as phantom command paths and the polish skill batch-fixed them\npost-hoc on every CLI with multi-word operation names.\n\nRoute the SKILL.md and README per-endpoint emission through the same\n`kebab` template helper and kebab-case the endpoint name in\n`firstCommandExample` so the first-command example block stays in sync\nwith the cobra command tree. Single-word endpoint keys pass through\nunchanged, so existing CLIs do not drift on regen.\n\nCloses #1270\n\n* fix(cli): apply README Commands promotion guard\n\nThe skill.md.tmpl \"Command Reference\" loop checks\nPromotedResourceNames and renders just the resource name when a\nsingle-endpoint resource is promoted to a top-level cobra command;\nthe readme.md.tmpl \"Commands\" loop did not, so a spec like\n{currencies: {list: ...}} produced `<cli> currencies list` in\nREADME while the binary only exposed `<cli> currencies`.\n\nMirror the guard so README and SKILL.md agree on the leaf form for\npromoted resources. Extend the integration test to cover this case,\nfix the now-correct README expectation in\nTestOutputFormatsUsesRealCommandExample, and accept the golden README\ndiffs for generate-golden-api and generate-golden-api-rich-auth where\nsingle-op resources (currencies/list, public/get-status, items/list)\nare promoted.\n\nRefs #1270"},{"hash":"5a04aca6","date":"2026-05-16 03:06:59 +0900","author":"cobuchan","subject":"fix(skills): correct install-internal-skills.sh path to repo skills/ (#1342)","body":"The installer resolved REPO_SKILLS to <repo>/.claude/skills/ (one level\ntoo shallow), causing the documented install command to abort with\n\"no skills found\". Fix the path and sync AGENTS.md so its referenced\nlocation matches the repo's actual top-level skills/ directory.\n\nFixes #869\n\nCo-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"096411ce","date":"2026-05-15 10:58:23 -0700","author":"Trevin Chow","subject":"fix(cli): strip root-level binaries from staged publish tree (#1449)","body":"* fix(cli): strip root-level binaries from staged publish tree\n\nThe publish skill's rm -f cleanup line listed two binary names but\nmissed <api-slug>-pp-mcp, so every CLI with MCP vision shipped a 21MB\nMach-O peer into the publish PR unless the operator caught it by hand.\n\nStrip the named root-level binaries (bare slug, <api-slug>-pp-cli,\n<cli-name>, <api-slug>-pp-mcp) in publish package after CopyDir, so\nthe staged tree is binary-free regardless of which path produced them\n(Makefile build / build-mcp / hand-run go build ./cmd/...). The SKILL\nrm -f line gets the pp-mcp entry too for the agent path.\n\nCloses #1396\n\n* docs(cli): clarify Greptile-flagged comments in publish-strip path\n\nGreptile pointed out two minor ambiguities:\n\n- `internal/cli/publish.go:364`: the phrase \"before the copy\" read as\n  \"before pipeline.CopyDir\" given the adjacent call, when the intended\n  reference was the skill's downstream `cp -r` step. Reword to name the\n  skill's `rm -f` parallel and the staged-copy timing explicitly.\n\n- `internal/cli/publish_test.go:582`: the inline comment said the skill\n  cleanup \"currently misses\" the pp-mcp binary, but this PR's SKILL.md\n  edit fixed that. Update to state both layers now cover it.\n\nNo code changes. Refs #1396."},{"hash":"b8b1b6a4","date":"2026-05-15 10:46:55 -0700","author":"Trevin Chow","subject":"fix(cli): force sync concurrency=1 under PRINTING_PRESS_VERIFY to avoid SQLITE_BUSY (#1441)","body":"* fix(cli): force sync concurrency=1 under PRINTING_PRESS_VERIFY to avoid SQLITE_BUSY\n\nUnder PRINTING_PRESS_VERIFY=1 (mock/dry-run), the generated sync command's\nworker pool used to race on SQLite writes once network latency disappeared,\ntripping SQLITE_BUSY despite _busy_timeout. validate-narrative --full-examples\nfailed any quickstart or recipe that exercised sync on a CLI with more than a\nhandful of resources.\n\nForce concurrency to 1 only when cliutil.IsVerifyEnv() returns true; real-world\nsync keeps the parallel worker pool unchanged.\n\nTouches sync.go.tmpl and graphql_sync.go.tmpl plus generator regression tests\nthat pin the import, the IsVerifyEnv() guard, and the after-default ordering.\nThree sync.go golden fixtures refresh accordingly.\n\nCloses #1205\n\n* fix(cli): correct misleading ordering rationale in sync verify-env test helper\n\nGreptile flagged the comment claim that placing the verify-env block before\nthe default-resolution block \"would reset the pin on every call.\" That's\nwrong — `1 < 1` is false, so the default block is a no-op once the verify\nblock sets concurrency to 1, regardless of ordering.\n\nReframe the assertion as a defensive guard against template churn rather\nthan a correctness invariant, and remove the false rationale from the\nhelper's docstring and assertion message."},{"hash":"dbfc6118","date":"2026-05-15 10:29:20 -0700","author":"Trevin Chow","subject":"fix(cli): propagate PRINTING_PRESS_VERIFY=1 to browser-session-proof probe (#1458)","body":"* fix(cli): propagate PRINTING_PRESS_VERIFY=1 to browser-session-proof probe\n\nrunBrowserSessionProofTest spawned doctor --json with subprocessEnv()\nalone, leaving PRINTING_PRESS_VERIFY unset. cliutil.IsVerifyEnv() then\nread false, doctor's synthetic browser-session-proof short-circuit\ndidn't fire, and every cookie/composed-auth CLI scored 0 on the probe.\nPolish skill correctly flagged this as environmental but shipcheck\nstill returned a misleading FAIL verdict.\n\nAppend PRINTING_PRESS_VERIFY=1 to the probe's env so the synthetic\nproof path runs without requiring the operator to export the var.\n\nCloses #1389\n\n* test(cli): fully unset PRINTING_PRESS_VERIFY before propagation test\n\nGreptile noted that t.Setenv(key, \"\") sets the variable to an empty\nstring rather than removing it. On platforms where Go's exec syscall\ntakes the first env occurrence (some Linux configurations), the empty\nPRINTING_PRESS_VERIFY= would shadow the probe's later\nPRINTING_PRESS_VERIFY=1, making the test silently vacuous.\n\nSwitch to os.Unsetenv with a cleanup that restores the prior value so\nthe env entry is genuinely absent for the duration of the test.\n\nRefs #1389"}]}