Skip to content

feat: copilot cli doctor#35

Merged
kenryu42 merged 10 commits intomainfrom
feat/copilot-cli-doctor
Mar 20, 2026
Merged

feat: copilot cli doctor#35
kenryu42 merged 10 commits intomainfrom
feat/copilot-cli-doctor

Conversation

@kenryu42
Copy link
Owner

@kenryu42 kenryu42 commented Mar 20, 2026

Summary

  • Add Copilot CLI as a new platform detected by doctor, supporting both hook-file and inline-hook configuration styles
  • Detect Copilot CLI version via --binary-version (with --version fallback) and display it in system info
  • Update README with comprehensive Copilot CLI setup documentation covering hook files, inline hooks, user-level hooks, and disableAllHooks

Changes

  • src/bin/doctor/hooks.ts — Added detectCopilot() detection function with support for:
    • Repository hook files (.github/hooks/*.json)
    • User hook files (~/.copilot/hooks/*.json, requires Copilot CLI 0.0.422+)
    • Inline hooks in Copilot config files (~/.copilot/config.json, .github/copilot/settings.json, .github/copilot/settings.local.json, requires Copilot CLI 1.0.8+)
    • disableAllHooks detection with correct precedence (local → repo → user)
    • COPILOT_HOME environment variable support
    • Semver comparison helpers (parseSemver, compareSemver) for feature-gating by version
    • New types: CopilotHookConfig, CopilotInlineConfigSource, CopilotDetectionState
  • src/bin/doctor/types.ts — Added 'copilot-cli' to HookPlatform union; added configPaths to HookStatus; added copilotCliVersion to SystemInfo
  • src/bin/doctor/system-info.ts — Added copilot --binary-version / --version fetching with fallback logic; fixed potential race in defaultVersionFetcher (double-resolve guard via isSettled)
  • src/bin/doctor/format.ts — Added 'copilot-cli': 'Copilot CLI' display name; added Copilot CLI row to system info table
  • src/bin/doctor/index.ts — Pass copilotCliVersion from system info into detectAllHooks options; reordered calls so system info is fetched before hook detection
  • README.md — Rewrote Copilot CLI section with Option A (hook files) and Option B (inline hooks), version requirements, disableAllHooks, and COPILOT_HOME notes
  • tests/bin/doctor/hooks.test.ts — Added ~536 lines of tests covering all Copilot CLI detection paths (hook files, inline hooks, version gating, disable flags, COPILOT_HOME, error handling, edge cases)
  • tests/bin/doctor/system-info.test.ts — Added tests for Copilot CLI version fetching (--binary-version preference, --version fallback, null handling)
  • tests/bin/doctor/format.test.ts — Added test for Copilot CLI display name rendering
  • tests/helpers.ts — Added copilot entry to mockVersionFetcher

Testing

bun run check

Additional manual verification:

# Run doctor to see Copilot CLI detection in action
bun src/bin/cc-safety-net.ts doctor

# Run only the new Copilot CLI tests
bun test tests/bin/doctor/hooks.test.ts
bun test tests/bin/doctor/system-info.test.ts
bun test tests/bin/doctor/format.test.ts

Related Issue

PR Checklist

  • I have read the CONTRIBUTING.md
  • Code follows project conventions (type hints, naming, etc.)
  • bun run check passes (lint, types, dead code, rules, tests)
  • Tests added for new rules (minimum 90% coverage required)
  • Tested locally with Claude Code, OpenCode, Gemini CLI or GitHub Copilot CLI
  • Updated documentation if needed (README, AGENTS.md)
  • No version changes in package.json

Summary by CodeRabbit

  • New Features

    • Doctor now detects and reports Copilot CLI hooks and shows Copilot CLI in system info
    • Copilot CLI version probing with a timeout and version-gated detection for supported hook styles
  • Documentation

    • Expanded README with two Copilot hook setup flows, discovery paths, cascade/override rules, COPILOT_HOME guidance, disableAllHooks behavior, and wording changed to “shell commands”
  • Tests

    • Added tests covering Copilot detection, version gating, parsing errors, precedence, formatting, and timeout behavior

- Detect Copilot CLI hooks from ~/.copilot/hooks/ and .github/hooks/
- Add Copilot CLI version detection with timeout handling
- Update README with global hook documentation
- Add comprehensive tests for Copilot CLI detection
Add documentation for both hook files and inline hooks config styles, including

version requirements and configuration precedence (user -> repo -> local).
Adds tests for:

- Global config.json inline hooks at minimum supported version

- Repository hooks path being a file instead of a directory
@coderabbitai
Copy link

coderabbitai bot commented Mar 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e7446e6-8d8a-4e83-9413-60c04db51d55

📥 Commits

Reviewing files that changed from the base of the PR and between faf5388 and b88c5a7.

⛔ Files ignored due to path filters (1)
  • dist/bin/cc-safety-net.js is excluded by !**/dist/**
📒 Files selected for processing (2)
  • src/bin/doctor/hooks.ts
  • tests/bin/doctor/hooks.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/bin/doctor/hooks.test.ts

📝 Walkthrough

Walkthrough

Adds GitHub Copilot CLI detection to the doctor command: probes Copilot CLI version with timeouts, discovers Copilot hook sources (repo/user hook files and inline settings), applies semver gating and disable precedence, runs self-test when configured, surfaces Copilot in formatted output, updates types, tests, and README docs.

Changes

Cohort / File(s) Summary
Documentation
README.md
Document two Copilot hook configuration styles (hook files vs inline hooks), supported paths (.github/hooks/*, ~/.copilot/hooks/*, ~/.copilot/config.json, .github/copilot/settings*.json), cascade/override order, disableAllHooks, COPILOT_HOME guidance, change “bash”→“shell”, and add JSON examples.
Types
src/bin/doctor/types.ts
Add 'copilot-cli' to HookPlatform; add optional configPaths?: readonly string[] to HookStatus; add `copilotCliVersion: string
System Version Detection
src/bin/doctor/system-info.ts
Add 2000ms timeout to version fetches, unify finish handler to avoid multiple resolves, probe copilot --binary-version then copilot --version fallback, and expose copilotCliVersion on SystemInfo.
Hook Detection Logic
src/bin/doctor/hooks.ts
Extend detectAllHooks API with copilotCliVersion?; scan repo .github/hooks/*.json and user <COPILOT_HOME>/hooks/*.json, parse inline copilot config files (settings.local.json, settings.json, config.json), semver-gate sources, detect Safety Net via command flags, respect disableAllHooks, accumulate configPaths and parse errors, and append a copilot-cli HookStatus.
Doctor Flow & Formatting
src/bin/doctor/index.ts, src/bin/doctor/format.ts
Await getSystemInfo() before hook detection and pass copilotCliVersion into detectAllHooks; include “Copilot CLI” in platform name mapping and system info output.
Tests & Helpers
tests/...
tests/bin/doctor/hooks.test.ts, tests/bin/doctor/format.test.ts, tests/bin/doctor/system-info.test.ts, tests/helpers.ts
Add extensive Copilot-focused tests (detection, version gating, malformed JSON, COPILOT_HOME handling, disable precedence, command key/flag parsing), update fixtures to include copilotCliVersion, add test helpers to write Copilot configs, and add mock Copilot version to mockVersionFetcher.
Formatting Output Tests
src/bin/doctor/format.ts, tests/bin/doctor/format.test.ts
Add Copilot CLI row to formatted system info; tests assert configPaths are not printed in human-readable hooks section.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as Doctor CLI
    participant Doctor as detectAllHooks()
    participant Sys as getSystemInfo()
    participant FS as File System
    participant Parser as Config Parser
    participant SemVer as SemVer Checker
    participant SelfTest as Self-Test Runner

    CLI->>Sys: getSystemInfo()
    Sys->>CLI: return SystemInfo (includes copilotCliVersion)

    CLI->>Doctor: detectAllHooks(cwd, {copilotCliVersion})
    Note over Doctor: discover sources: repo hooks, user hooks, inline settings

    Doctor->>FS: read `.github/hooks/*.json`
    FS-->>Doctor: file list / contents
    Doctor->>Parser: parse each hook file
    Parser-->>Doctor: configs / parse errors

    Doctor->>FS: read `<COPILOT_HOME>/hooks/*.json`
    FS-->>Doctor: file list / contents
    Doctor->>Parser: parse each hook file
    Parser-->>Doctor: configs / parse errors

    Doctor->>FS: read inline config files (`settings.local.json`, `settings.json`, `config.json`)
    FS-->>Doctor: contents
    Doctor->>Parser: parse inline configs
    Parser-->>Doctor: configs / parse errors

    Doctor->>SemVer: check copilotCliVersion against required thresholds
    SemVer-->>Doctor: supported / unsupported

    alt supported & relevant config found
        Doctor->>Parser: evaluate disableAllHooks precedence
        alt disabled
            Doctor-->>CLI: status 'disabled' (include configPaths/errors)
        else
            Doctor->>SelfTest: runSelfTest() for Safety Net
            SelfTest-->>Doctor: pass/fail
            Doctor-->>CLI: status 'configured' (include configPaths/errors)
        end
    else unsupported or none found
        Doctor-->>CLI: status 'n/a' (include errors if any)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hop through configs, sniffing every trail,
Repo, user, local — I follow the trail.
Semver gates and timeouts keep my nose precise,
Found the Safety Net — I thump twice, then slice! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: copilot cli doctor' clearly summarizes the main change: adding Copilot CLI detection to the doctor tool.
Description check ✅ Passed The PR description is comprehensive, covering summary, detailed changes, testing instructions, and a completed checklist matching the template requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/copilot-cli-doctor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Mar 20, 2026

Greptile Summary

This PR extends the doctor command with full GitHub Copilot CLI hook detection, surfacing both traditional hook files (.github/hooks/*.json, ~/.copilot/hooks/*.json) and the newer inline-hook format inside Copilot settings files, with correct version-gating (0.0.422+ for user hook files, 1.0.8+ for inline hooks) and disableAllHooks precedence (local → repo → user).

Key changes:

  • hooks.ts — New _checkCopilotEnabled helper with semver helpers (_parseSemver, _compareSemver), file collectors, and the full detection state machine; version-gated errors are cleanly suppressed by passing undefined as the error array when a feature is unsupported.
  • system-info.ts — Fetches Copilot CLI version using both --binary-version and --version concurrently, preferring --binary-version; also adds a 2000ms timeout and isSettled double-resolve guard to defaultVersionFetcher.
  • index.tsgetSystemInfo is now awaited before detectAllHooks so copilotCliVersion is available for version-gated detection paths.
  • Tests — ~536 lines of new integration tests covering all detection paths, version gates, disableAllHooks precedence, COPILOT_HOME, and error handling.

Issue to address: The disableAllHooks check in _checkCopilotEnabled is guarded by inlineSupport === true (line 596). When copilotCliVersion is null (binary absent or version unparseable) and traditional hook files are present, any disableAllHooks: true in inline settings is silently ignored — the doctor incorrectly reports status: 'configured', giving false confidence that the Safety Net is active when Copilot CLI would actually disable it. The test suite also lacks a case for this specific combination.

Confidence Score: 3/5

  • Safe to merge after addressing the disableAllHooks false-positive when version is null
  • The detection logic is well-structured and thoroughly tested across most paths. One P1 logic gap exists: when copilotCliVersion is null, inline disableAllHooks: true is not checked even though traditional hook files are still detected, so the doctor can report "configured" when the actual runtime would honor the disable flag. The corresponding test for this combination is also missing. The style/fragility concerns (endsWith ordering, single-warning break) are minor and don't affect correctness in current usage.
  • Pay close attention to src/bin/doctor/hooks.ts, specifically the _checkCopilotEnabled function's disableAllHooks guard at line 596.

Important Files Changed

Filename Overview
src/bin/doctor/hooks.ts Core of the PR — adds full Copilot CLI detection with version-gated logic for hook files and inline configs; the disableAllHooks guard is skipped when version is null, which can produce a false "configured" status.
src/bin/doctor/system-info.ts Adds concurrent Copilot CLI version probing via --binary-version (with --version fallback); fixes a pre-existing double-resolve race with isSettled guard and adds a 2s timeout — overall solid.
src/bin/doctor/index.ts Reorders getSystemInfo before detectAllHooks so copilotCliVersion is available for version-gated detection; straightforward and correct.
src/bin/doctor/types.ts Adds copilot-cli to HookPlatform, configPaths to HookStatus, and copilotCliVersion to SystemInfo — clean, additive-only changes with no regressions.
tests/bin/doctor/hooks.test.ts Comprehensive test suite (~536 lines) covering hook files, inline hooks, version gating, disableAllHooks precedence, COPILOT_HOME, and error handling; one missing case is the null-version + traditional hook + inline disableAllHooks: true combination.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[detectCopilot called] --> B[_getCopilotConfigHome]
    B --> C[_supportsCopilotInlineHooks\nversion]
    C --> D{inlineSupport?}

    D -- "true (≥1.0.8)" --> E[collectCopilotInlineConfig\nfor all 3 sources\nerrors forwarded]
    D -- "false / null" --> F[collectCopilotInlineConfig\nfor all 3 sources\nerrors suppressed]

    E --> G{_resolveCopilotInlineDisableSource}
    G -- "disableAllHooks: true found" --> H[return disabledBy\nstatus: disabled]
    G -- "not disabled" --> I[collect repoHookPaths]

    F --> I

    I --> J[_supportsCopilotUserHookFiles\nversion]
    J --> K{userHookSupport?}
    K -- "true (≥0.0.422)" --> L[collect userHookPaths\nerrors forwarded]
    K -- "false / null" --> M[collect userHookPaths\nerrors suppressed]

    L --> N{Safety Net hooks\nin user files?}
    M --> N

    N -- "yes & support!=true" --> O[warn unsupported\nclear userHookPaths]
    N -- "yes & support==true" --> P[keep userHookPaths]
    N -- "no" --> P

    O --> Q[iterate inlineSourcesByPrecedence]
    P --> Q

    Q --> R{Safety Net hook\nin source?}
    R -- "no" --> Q
    R -- "yes & inlineSupport==true" --> S[add to inlinePaths]
    R -- "yes & inlineSupport!=true" --> T[warn unsupported\nbreak loop]

    S --> Q
    T --> U[build activeConfigPaths\nlocal→repo→hookFiles→user]
    Q -- done --> U

    U --> V{activeConfigPaths\nnon-empty?}
    V -- "yes" --> W[return configured\n+ selfTest]
    V -- "no" --> X[return n/a]
Loading

Comments Outside Diff (1)

  1. src/bin/doctor/hooks.ts, line 730-748 (link)

    P2 Single warning emitted for multiple unsupported inline sources

    When inlineSupport !== true and more than one entry in inlineSourcesByPrecedence contains a Safety Net hook, the break on line 748 causes only one warning to be emitted. A user with Safety Net hooks in both settings.json and ~/.copilot/config.json on an older version sees only a single generic message and has no indication that two separate files are affected.

    If the desired UX is one consolidated warning (to avoid noisy duplication), the current design is intentional. But if that is the case, it would be good to add a comment explaining the deliberate break so future contributors don't inadvertently change the behavior:

        // Emit only one warning regardless of how many sources have Safety Net hooks;
        // the message is the same for all, so additional messages would be noise.
        _warnOnUnsupportedCopilotSource(...);
        break;

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Last reviewed commit: "docs: clarify Copilo..."

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
tests/helpers.ts (1)

66-70: Mark mockVersionFetcher as internal.

This helper is exported only for tests. Adding the standard JSDoc here keeps knip from treating it as public API.

Suggested tweak
 /**
  * Mock version fetcher for testing.
  * Returns predefined versions instantly without spawning processes.
+ * `@internal` Exported for testing
  */
 export const mockVersionFetcher: VersionFetcher = async (args: string[]) => {

As per coding guidelines: For test-only exports, add /** @internal Exported for testing */ JSDoc comment to satisfy knip.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/helpers.ts` around lines 66 - 70, Mark the exported test helper
mockVersionFetcher as internal by adding the JSDoc tag /** `@internal` Exported
for testing */ immediately above the export declaration for mockVersionFetcher
so knip treats it as non-public; locate the export const mockVersionFetcher:
VersionFetcher = ... and insert that comment directly above it.
src/bin/doctor/types.ts (1)

42-42: Make configPaths readonly.

HookStatus is report data, so callers should not need to mutate this array. Using readonly string[] preserves assignability for producers and makes the new field safer for consumers.

Suggested tweak
-  configPaths?: string[];
+  configPaths?: readonly string[];

As per coding guidelines: Use readonly arrays where mutation isn't needed in TypeScript.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bin/doctor/types.ts` at line 42, HookStatus currently exposes configPaths
as a mutable array (configPaths?: string[]) but this is report data and should
be immutable for consumers; update the HookStatus type declaration to use a
readonly array (configPaths?: readonly string[]) so producers can still assign
plain string[] while preventing downstream mutation, and ensure any code
constructing HookStatus continues to accept/assign string[] without changes to
runtime behavior.
tests/bin/doctor/hooks.test.ts (1)

12-60: Prefix the local test helpers with _.

writeCopilotHook and writeCopilotInlineConfig are file-local helpers, so _writeCopilotHook / _writeCopilotInlineConfig would match the repo’s TS naming rule.

As per coding guidelines, "Use leading underscore for private/internal module functions in TypeScript".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/bin/doctor/hooks.test.ts` around lines 12 - 60, Rename the two
file-local helper functions to use a leading underscore to follow the TS naming
rule: change writeCopilotHook -> _writeCopilotHook and writeCopilotInlineConfig
-> _writeCopilotInlineConfig, and update all internal references/calls within
the same file (tests/bin/doctor/hooks.test.ts) to the new names; keep their
signatures and behavior identical.
src/bin/doctor/hooks.ts (1)

426-658: Prefix the new module-private helpers with _.

These helpers are internal to this module, so public-style names drift from the repo’s TypeScript naming convention.

As per coding guidelines, "Use leading underscore for private/internal module functions in TypeScript".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bin/doctor/hooks.ts` around lines 426 - 658, The helpers in this module
should be renamed to use a leading underscore to mark them as module-private;
rename the functions isSafetyNetCopilotCommand, parseSemver, compareSemver,
supportsCopilotUserHookFiles, supportsCopilotInlineHooks, getCopilotConfigHome,
hasSafetyNetCopilotHook, readCopilotConfigFile, listJsonFiles,
collectSafetyNetCopilotHookFiles, collectCopilotInlineConfig,
warnOnUnsupportedCopilotSource, resolveCopilotInlineDisableSource, and
checkCopilotEnabled to _isSafetyNetCopilotCommand, _parseSemver, _compareSemver,
_supportsCopilotUserHookFiles, _supportsCopilotInlineHooks,
_getCopilotConfigHome, _hasSafetyNetCopilotHook, _readCopilotConfigFile,
_listJsonFiles, _collectSafetyNetCopilotHookFiles, _collectCopilotInlineConfig,
_warnOnUnsupportedCopilotSource, _resolveCopilotInlineDisableSource, and
_checkCopilotEnabled respectively, and update every call site in this file to
use the new names so imports/exports aren’t affected. Ensure there are no
lingering references to the old names and run type-checks to catch any missed
usages.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 212-223: Update the README text that currently reads "Copilot
settings cascade from user -> repository -> local. `disableAllHooks: true`
disables both repo-level and user-level hooks." to explicitly state the
precedence direction so it's unambiguous which setting wins; for example, change
it to something like "Copilot settings cascade from user -> repository -> local
(later files override earlier ones — i.e., local overrides repository overrides
user). `disableAllHooks: true` disables both repo-level and user-level hooks."
Make this edit around the sentence containing "Copilot settings cascade from
user -> repository -> local" and keep the `disableAllHooks` reference intact.

In `@src/bin/doctor/hooks.ts`:
- Around line 614-619: The warning currently hardcodes '~/.copilot/hooks';
update the call to warnOnUnsupportedCopilotSource so it uses the resolved
Copilot home path (e.g. pass userHookDir or configHome) instead of the literal
string. Locate the call to warnOnUnsupportedCopilotSource in this file (the
invocation with errors and copilotCliVersion) and replace the hardcoded 'user
hook files in ~/.copilot/hooks' message with a message that includes the
resolved userHookDir/configHome variable so the doctor points to the actual hook
directory.
- Around line 602-620: The warning for unsupported Copilot sources should only
be emitted when at least one config actually affects Safety Net; instead of
warning whenever there are JSON files, scan the userHookFiles with
readCopilotConfigFile and hasSafetyNetCopilotHook and only call
warnOnUnsupportedCopilotSource if any file returns true (update the logic around
userHookFiles/userHookSupport/userHookPaths and similarly the other block at
630-646 that handles inline Copilot configs), keeping the existing
paths/variables (userHookDir, userHookFiles, userHookPaths,
readCopilotConfigFile, hasSafetyNetCopilotHook, warnOnUnsupportedCopilotSource,
copilotCliVersion) to locate and gate the warning.

---

Nitpick comments:
In `@src/bin/doctor/hooks.ts`:
- Around line 426-658: The helpers in this module should be renamed to use a
leading underscore to mark them as module-private; rename the functions
isSafetyNetCopilotCommand, parseSemver, compareSemver,
supportsCopilotUserHookFiles, supportsCopilotInlineHooks, getCopilotConfigHome,
hasSafetyNetCopilotHook, readCopilotConfigFile, listJsonFiles,
collectSafetyNetCopilotHookFiles, collectCopilotInlineConfig,
warnOnUnsupportedCopilotSource, resolveCopilotInlineDisableSource, and
checkCopilotEnabled to _isSafetyNetCopilotCommand, _parseSemver, _compareSemver,
_supportsCopilotUserHookFiles, _supportsCopilotInlineHooks,
_getCopilotConfigHome, _hasSafetyNetCopilotHook, _readCopilotConfigFile,
_listJsonFiles, _collectSafetyNetCopilotHookFiles, _collectCopilotInlineConfig,
_warnOnUnsupportedCopilotSource, _resolveCopilotInlineDisableSource, and
_checkCopilotEnabled respectively, and update every call site in this file to
use the new names so imports/exports aren’t affected. Ensure there are no
lingering references to the old names and run type-checks to catch any missed
usages.

In `@src/bin/doctor/types.ts`:
- Line 42: HookStatus currently exposes configPaths as a mutable array
(configPaths?: string[]) but this is report data and should be immutable for
consumers; update the HookStatus type declaration to use a readonly array
(configPaths?: readonly string[]) so producers can still assign plain string[]
while preventing downstream mutation, and ensure any code constructing
HookStatus continues to accept/assign string[] without changes to runtime
behavior.

In `@tests/bin/doctor/hooks.test.ts`:
- Around line 12-60: Rename the two file-local helper functions to use a leading
underscore to follow the TS naming rule: change writeCopilotHook ->
_writeCopilotHook and writeCopilotInlineConfig -> _writeCopilotInlineConfig, and
update all internal references/calls within the same file
(tests/bin/doctor/hooks.test.ts) to the new names; keep their signatures and
behavior identical.

In `@tests/helpers.ts`:
- Around line 66-70: Mark the exported test helper mockVersionFetcher as
internal by adding the JSDoc tag /** `@internal` Exported for testing */
immediately above the export declaration for mockVersionFetcher so knip treats
it as non-public; locate the export const mockVersionFetcher: VersionFetcher =
... and insert that comment directly above it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ca34aa8c-8685-4059-8271-b38436fb8369

📥 Commits

Reviewing files that changed from the base of the PR and between 46a35a7 and 13a99cc.

⛔ Files ignored due to path filters (3)
  • dist/bin/cc-safety-net.js is excluded by !**/dist/**
  • dist/bin/doctor/hooks.d.ts is excluded by !**/dist/**
  • dist/bin/doctor/types.d.ts is excluded by !**/dist/**
📒 Files selected for processing (10)
  • README.md
  • src/bin/doctor/format.ts
  • src/bin/doctor/hooks.ts
  • src/bin/doctor/index.ts
  • src/bin/doctor/system-info.ts
  • src/bin/doctor/types.ts
  • tests/bin/doctor/format.test.ts
  • tests/bin/doctor/hooks.test.ts
  • tests/bin/doctor/system-info.test.ts
  • tests/helpers.ts

@codecov
Copy link

codecov bot commented Mar 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.53%. Comparing base (46a35a7) to head (b88c5a7).
⚠️ Report is 11 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #35      +/-   ##
==========================================
+ Coverage   99.50%   99.53%   +0.02%     
==========================================
  Files          50       50              
  Lines        5085     5330     +245     
==========================================
+ Hits         5060     5305     +245     
  Misses         25       25              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/bin/doctor/system-info.test.ts (1)

106-136: Consider increasing the test timeout margin.

The test uses a 100ms timeout (}, 100)) to verify that getSystemInfo doesn't wait for the fallback --version probe when --binary-version succeeds. While this works, the tight timeout could cause flaky failures on slower CI runners.

💡 Suggestion
-  }, 100);
+  }, 500);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/bin/doctor/system-info.test.ts` around lines 106 - 136, The test "does
not wait for copilot --version when --binary-version succeeds" uses a 100ms Jest
timeout which is too tight and can cause flakes on slow CI; update the test
timeout (the final argument to the test declaration) to a larger value (e.g.,
1000ms or 2000ms) so getSystemInfo(fetcher) with deferred promises
(binaryVersion and fallbackVersion) has enough time to resolve and assertions on
copilotCliVersion succeed reliably.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/bin/doctor/system-info.test.ts`:
- Around line 106-136: The test "does not wait for copilot --version when
--binary-version succeeds" uses a 100ms Jest timeout which is too tight and can
cause flakes on slow CI; update the test timeout (the final argument to the test
declaration) to a larger value (e.g., 1000ms or 2000ms) so
getSystemInfo(fetcher) with deferred promises (binaryVersion and
fallbackVersion) has enough time to resolve and assertions on copilotCliVersion
succeed reliably.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d65e498c-7ab2-434d-806b-765ab876af83

📥 Commits

Reviewing files that changed from the base of the PR and between 13a99cc and faf5388.

⛔ Files ignored due to path filters (2)
  • dist/bin/cc-safety-net.js is excluded by !**/dist/**
  • dist/bin/doctor/types.d.ts is excluded by !**/dist/**
📒 Files selected for processing (7)
  • README.md
  • src/bin/doctor/hooks.ts
  • src/bin/doctor/system-info.ts
  • src/bin/doctor/types.ts
  • tests/bin/doctor/hooks.test.ts
  • tests/bin/doctor/system-info.test.ts
  • tests/helpers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/helpers.ts

@kenryu42
Copy link
Owner Author

@greptileai review the PR

Comment on lines +596 to +601
if (inlineSupport === true) {
const disableSource = _resolveCopilotInlineDisableSource(inlineSources);
if (disableSource) {
return { activeConfigPaths: [], disabledBy: disableSource };
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 disableAllHooks ignored when copilotCliVersion is null

The disableAllHooks check (lines 596–601) is guarded by inlineSupport === true, meaning it is entirely skipped when copilotCliVersion is null (binary absent, spawn failed, or version unparseable). In that scenario the code continues past this guard, finds any matching traditional hook files in .github/hooks/, and returns status: 'configured' — even when a disableAllHooks: true entry is present in an inline settings file that the actual Copilot CLI runtime would honor (if the binary is >= 1.0.8).

Concrete failing scenario:

  1. User on Copilot CLI 1.2.0, but copilot --binary-version and copilot --version both fail to produce a parseable string → copilotCliVersion = null
  2. .github/hooks/safety-net.json exists ✓
  3. .github/copilot/settings.json contains { "disableAllHooks": true }

Doctor reports status: 'configured' (false positive) while the runtime actually disables all hooks.

A safe path is to check disableAllHooks whenever inlineSupport !== false (i.e., unknown or supported), and surface a warning when the disable was found but the version is unknown so the user can act:

// Check disableAllHooks whenever version doesn't rule out inline support
if (inlineSupport !== false) {
  const disableSource = _resolveCopilotInlineDisableSource(inlineSources);
  if (disableSource) {
    if (inlineSupport === null) {
      errors.push(
        `disableAllHooks: true found in ${disableSource}, but Copilot CLI version is unknown; reporting as disabled to be safe`,
      );
    }
    return { activeConfigPaths: [], disabledBy: disableSource };
  }
}

Alternatively, add a test covering this path (copilotCliVersion = null, traditional hook file present, inline disableAllHooks: true present) to at least document the known limitation explicitly.

Comment on lines +651 to +659
return {
activeConfigPaths: [
...inlinePaths.filter((path) => path.endsWith('settings.local.json')),
...inlinePaths.filter((path) => path.endsWith('settings.json')),
...repoHookPaths,
...inlinePaths.filter((path) => path.endsWith('config.json')),
...userHookPaths,
],
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 activeConfigPaths ordering relies on endsWith filename suffixes

The three inlinePaths.filter calls categorize paths by suffix ('settings.local.json', 'settings.json', 'config.json'). This works correctly for the three hardcoded source paths today, but it is fragile: a non-standard COPILOT_HOME whose path itself ends in one of these suffixes, or any future path addition, could silently mis-sort entries.

Consider tracking the category alongside each path (e.g. a tuple [path, 'local' | 'repo' | 'user']) as it is assembled in the inlineSourcesByPrecedence loop, then sorting on the category label rather than the file name suffix. That makes the intent explicit and removes the implicit coupling to the specific file names.

@kenryu42 kenryu42 merged commit 7cf01eb into main Mar 20, 2026
8 checks passed
@kenryu42 kenryu42 deleted the feat/copilot-cli-doctor branch March 20, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant