This is the opt-in surface for the new go-gh aggressive caching
support. Setting GH_AGGRESSIVE_CACHING_TTL to a Go duration string
(e.g. 30s, 1m, 5m, 1h) enables process-wide TTL caching for cacheable
REST and GraphQL requests on the standard authenticated HTTP client.
Motivation: heavy users running multiple parallel agent harnesses
(e.g. several Codex agents in lockstep) issue duplicate identical
requests within seconds of each other, exhausting the GitHub primary
REST rate limit and tripping secondary limits. A short TTL (~30s)
dedupes the lockstep traffic without changing what gh fundamentally
is. The trade-off (bounded staleness) is acceptable when explicitly
opted into.
Wiring:
- api/http_client.go: NewHTTPClient reads GH_AGGRESSIVE_CACHING_TTL
after honoring caller-supplied EnableCache. The env var only
activates when:
- opts.EnableCache is not already true (caller config wins)
- opts.SkipDefaultHeaders is false (PlainHttpClient bypasses
defaults and must not opt into caching by env var)
Per-request X-GH-CACHE-TTL: 0 still bypasses for individual calls.
- parseAggressiveCachingTTL is silent on failure: empty, '0',
negative, or unparseable values disable the feature. Matches the
convention for other GH_* env vars and avoids stderr noise from
a transport layer. Users diagnose via the X-GH-Cache-Status
response header.
- help_topic.go: documents the env var, format, staleness caveat,
intended use case, observability hook, cleanup path
(gh config clear-cache), and explicit precedence rules.
Tests cover:
- parseAggressiveCachingTTL edge cases (empty, valid, zero,
negative, unparseable, missing unit, plain number).
- Env var with positive duration enables caching end-to-end
(miss -> hit pattern, network call only on first request).
- Env var unset or with invalid value leaves caching disabled.
- Caller-set EnableCache + CacheTTL takes precedence over env var.
- SkipDefaultHeaders (PlainHttpClient path) bypasses aggressive
caching even when env var is set.
- Per-request X-GH-CACHE-TTL: 0 hard-bypasses cache when env var
is set, and does not pollute the cached entry.
Manual smoke verified against live api.github.com:
- REST GET: miss -> hit transitions work, X-GH-Cache-Status
visible via 'gh api -i'.
- GraphQL query: same miss -> hit pattern works.
- Per-request opt-out: header returns no X-GH-Cache-Status,
confirming bypass.
- 'gh auth status', 'gh config clear-cache' continue to work.
Out of scope here:
- ETag / If-None-Match revalidation (planned follow-up that layers
on top of the same transport).
- Rewriting commands like 'gh pr checks --watch' to take advantage
of REST + ETag (separate decision, separate PR).
Depends on go-gh changes:
- 'Fix per-request cache opt-out when global TTL is configured'
- 'Restrict cacheable responses to 2xx'
- 'Skip caching of GraphQL mutations and subscriptions'
- 'Use atomic temp-file + rename for cache writes'
- 'Vary cache key by Accept-Encoding, X-GitHub-Api-Version, ...'
- 'Emit X-GH-Cache-Status response header for cache observability'
Related: cli/cli#13279, cli/cli#13293
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The four tests in this file (TestVerifyIntegration,
TestVerifyIntegrationCustomIssuer, TestVerifyIntegrationReusableWorkflow,
TestVerifyIntegrationReusableWorkflowSignerWorkflow) call
NewLiveSigstoreVerifier which requires network access to Sigstore and
GitHub TUF servers. Unlike the other integration test files in this
package (attestation_integration_test.go, sigstore_integration_test.go,
inspect_integration_test.go), this file was missing the //go:build
integration tag, causing these tests to run during a regular
'go test ./...' and fail in network-isolated build environments.
The review hint printed after `gh skill install --allow-hidden-dirs`
suggests `gh skill preview` commands. Those commands would fail for
hidden-dir skills because preview would filter them out. Pass the
flag through so the suggested commands work as-is.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Most agent clients (Claude Code, Copilot, etc.) only discover immediate
subdirectories of their skills folder. When a skill repository used
namespaced paths like skills/author/my-skill/, the installer created
nested directories (e.g. .claude/skills/author/my-skill/) that clients
could not find.
This separates the skill's identity (InstallName, used for lockfile keys,
search, filtering, display) from the filesystem path (Name, used for the
install directory). Skills are now always installed flat:
.claude/skills/my-skill/SKILL.md (not .claude/skills/author/my-skill/)
Changes:
- installer: use skill.Name for directory paths instead of InstallName
- install.go: use skill.Name for overwrite checks and prompts
- collisions: detect conflicts by Name since flat install means two
skills with the same Name but different Namespace values will collide
- update: clean up old namespaced directories when migrating to flat
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add support for the --allow-hidden-dirs flag in `gh skill preview`,
matching the existing pattern in `gh skill install`. This allows users
to preview skills located in hidden directories (e.g. .claude/skills/,
.agents/skills/).
Changes:
- Add AllowHiddenDirs field to PreviewOptions
- Register --allow-hidden-dirs flag on the preview command
- Switch from DiscoverSkills to DiscoverSkillsWithOptions to get all
skills including hidden-dir ones
- Add filterHiddenDirSkills to exclude hidden-dir skills by default,
showing a hint when they are found but excluded
- Print a warning when --allow-hidden-dirs is used and hidden skills
are present
- Return an error when only hidden-dir skills exist without the flag
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Widen ValidateSupportedHost to accept tenancy hosts (*.ghe.com) alongside
github.com. GHEC with data residency uses these domains, and all skill
subcommands (search, install, preview, publish, update) now allow them.
GitHub Enterprise Server remains unsupported and is explicitly rejected
with a clear error message.
Also fix the lockfile writer to use the actual host when constructing
SourceURL instead of hardcoding github.com.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When installing a skill whose SKILL.md contains github-repo metadata
pointing to a different repository, the CLI detects it as a re-published
skill and offers to redirect the install to the upstream source.
In interactive mode, the user is prompted to choose between the
re-publisher (default) and the upstream. In non-interactive mode,
the install proceeds from the re-publisher with a notice.
The --upstream flag skips the prompt and redirects to upstream directly,
enabling non-interactive upstream installs in CI/scripts.
If the user chooses upstream, the install restarts from that repo,
resolving the latest version and discovering skills fresh. A
skill_upstream_redirect telemetry event is emitted to track redirects.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use a two-pass search so exact DisplayName and Name matches are
preferred over InstallName. This avoids incorrectly selecting a
plugins-convention skill via InstallName when a standard namespaced
skill with a matching DisplayName exists later in the sorted slice.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The preview command's selectSkill function only matched skills by
DisplayName() and Name. For plugins-convention skills, the install
hint outputs InstallName() (namespace/name), which matched neither -
DisplayName() includes a [plugins] prefix and Name is just the base
name. This caused 'skill not found' errors when users ran the
suggested preview command after install.
Add InstallName() as an additional match criterion so that namespaced
skill identifiers produced by the install hint are accepted.
Closes#13248
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Relax skill discovery to recognize skills/ directories at any depth
in the repository tree, not just at the root. This enables repos like
hashicorp/agent-skills that organize skills under prefixes such as
terraform/code-generation/skills/*/SKILL.md.
Changes:
- matchSkillConventions: add checks for <prefix>/skills/<name>/SKILL.md
and <prefix>/skills/<ns>/<name>/SKILL.md at any depth
- isSkillPath: also match paths containing /skills/ for direct
path-based install
- DiscoverSkillByPath: fix namespace detection to find the skills
segment anywhere in the path
- Update error messages and help text to mention nested conventions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`gh auth refresh` exists to make it simpler for users to refresh their tokens on expiration/scope mismatch, but help messages only suggest using it in limited scenarios, and not in a common case of a token expiring and the user receiving a 401 error. Now, the auth flow will detect this case, and for refreshable tokens (namely, tokens created by logging in with `gh auth login` in the first place), it will suggest using `gh auth refresh` for these cases.
- --fix and --dry-run now error when combined
- "Ready to publish!" is suppressed when --fix is set since user must
commit first
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When --fix is used, return early after applying fixes instead of
continuing to the publish flow. The fixed files need to be committed
before publishing, so proceeding would fail anyway since the repo
would not be in sync.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move --allow-hidden-dirs filtering logic from the discovery package to
the install command, addressing review feedback. Discovery functions now
always return all skills (including hidden-dir), and callers decide how
to handle them.
Changes:
- DiscoverSkillsWithOptions/DiscoverLocalSkillsWithOptions always return
hidden-dir skills; callers filter using IsHiddenDirConvention()
- DiscoverSkills/DiscoverLocalSkills (convenience wrappers) auto-filter
hidden-dir skills for backward compatibility with preview/update/publish
- Remove --allow-hidden-dirs reference from discovery error messages
- Add filterHiddenDirSkills in install.go with caller-side flag logic
- Inline warning using heredoc.Docf, remove printHiddenDirWarning
- Add inline comments in matchHiddenDirConventions (babakks nitpicks)
- Add non-hidden-namespaced dir and no-skills-at-all test cases
- Add --allow-hidden-dirs tests in TestNewCmdInstall, TestInstallRun,
and TestRunLocalInstall
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* add support for installation in multiple agent host
* print correct dir in warning
* remove dir as it depends on user vs project installation scope
* Move comment closer to assertion in registry test
Move the explanatory comment from above the map initialization to
directly above the assertions it describes, per review feedback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* List supported agent names and IDs in help text
Replace the self-referencing "run --help" sentence with an inline list
of all supported --agent values showing Name (id) pairs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Use heredoc.Docf for Kiro CLI post-install hint
Replace individual fmt.Fprintln calls with a single heredoc.Docf block
for the Kiro CLI post-install guidance, per review feedback. Also
shorten the --agent flag usage line by overriding the auto-generated
enum list with a reference to the supported values in the help text.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Kynan Ware <47394200+BagToad@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add skills specific telemetry
* Remove VisibilityFuture, inline goroutine at call sites
The VisibilityFuture/FetchRepoVisibilityAsync/Wait wrapper was an
unidiomatic async abstraction built for a single pattern used in
exactly two call sites. In Go the channel is already the future;
wrapping it in a struct with a Wait(timeout) method adds no value.
Delete the abstraction and inline a local visResult struct,
buffered channel, goroutine, and select at each call site. Behavior
is preserved exactly: err -> "unknown", timeout -> "unknown",
success+public -> include skill_names.
FetchRepoVisibility (synchronous) is kept as-is.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix nonsense copilot tests
* Update telemetry tests for public-only dims and search event removal
Production telemetry emission changed:
- preview: skill_owner/skill_repo/skill_name (renamed from skill_names)
are now emitted only when repo_visibility=public.
- install: skill_owner/skill_repo/skill_names are now emitted only
when repo_visibility=public.
- search: the initial skill_search event was removed entirely; the
skill_search_install event no longer carries query/owner dims.
Update tests to match: rename skill_names -> skill_name in preview,
make owner/repo assertions conditional on public visibility in both
preview and install, and reduce the search test to a single event
with explicit Empty assertions for the removed query/owner dims so a
privacy regression cannot pass silently.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Test CategorizeHost and switch telemetry to skill_host_type
Add TestCategorizeHost covering all four classification branches
(github.com, ghes, tenancy, uncategorized) with cases verified
against the real ghauth implementation rather than guessed.
Update install and preview unit tests to assert the new
skill_host_type dimension name, and fix a typo in the preview
acceptance txtar (skill_hos_type -> skill_host_type).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Shrink visibility wait and test unknown visibility
The 2s visibilityWaitTimeout was wildly overprovisioned: by the time
telemetry emission reaches the select, the command has already done
several serial GitHub REST calls (and for install, a git sparse-checkout
plus possibly interactive prompts), so the one-call visibility fetch
has almost always completed. Drop the timeout to 200ms — a short safety
net for a stalled REST call, not a wait budget for a healthy one.
Also adds a table-driven case to TestFetchRepoVisibility covering an
unknown/future visibility value from the API, addressing @babakks'
review nitpick.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Harden SpawnSendTelemetry against relative executable paths
- Use io.Copy for telemetry subprocess stdin write
- Clean up GH_TELEMETRY/DO_NOT_TRACK help text
- Fall back to built-in defaults (NoOp telemetry) on config load failure
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace all exec.Command("git", ...), initGitRepo, runGitInDir, and
newTestGitClientWithUpstream with run.Stub()/run.CommandStubber stubs.
Changes:
- Remove os/exec and strings imports; add fmt, regexp, internal/run
- Add newTestGitClient(), stubGitRemote(), stubEnsurePushed() helpers
- Remove initGitRepo, runGitInDir, newTestGitClientWithUpstream helpers
- Add cmdStubs field to TestPublishRun table struct
- Convert all test cases to use stub-based git interactions
- Use regexp.QuoteMeta for remote name patterns
- Use %[1]s/%[2]s format args in stubGitRemote
- Initialize git.Client with explicit GitPath to avoid real git resolution
- Rewrite TestEnsurePushed with stub-based tests
- Update TestDetectGitHubRemote_UsesDir and TestPublishRun_DirArgUsesTargetRemote
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Return nil instead of real http.Client in unsupported host test
- Move skillResultKey type inside deduplicateResults function scope
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes post-install extension dispatch to keep the stub focused on
installation. Also removes unused args parameter from the run function.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Use cmdutil.DisableAuthCheck instead of raw annotation map
- Convert tests to table-driven format
- Use generic extension name in tests (gh-cool)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Register hidden stub commands for official GitHub extensions (gh-aw,
gh-stack) that offer to install the extension when invoked. This
replaces the error-string-matching approach from the original PR with
proper cobra commands that:
- Avoid false-positive matches on flag values or post-'--' args
- Eliminate conflicting cobra 'Did you mean?' suggestions
- Properly propagate prompt/install errors for correct exit codes
- Are hidden from help output and shell completions
- Use GroupID "extension" so checkValidExtension allows installing over them
- Are registered after extensions and aliases so both take priority
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>