Skip to content

aitools: typed fetch, state and agent CLI timeout errors - #7475

Open
anton-107 wants to merge 1 commit into
aitools/plugin-ref-fallbackfrom
aitools/typed-install-errors
Open

anton-107 wants to merge 1 commit into
aitools/plugin-ref-fallbackfrom
aitools/typed-install-errors

Conversation

@anton-107

@anton-107 anton-107 commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #7474.

Stacked on: aitools: do not block plugin installs on the latest-release lookup

Summary

aitools install failures that used to be UNCATEGORIZED now get a specific error_category. The installer returns typed errors and classifyInstallError maps them:

classifyInstallError(err)
  BlockedError
    TimedOut                     -> PLUGIN_CLI_TIMEOUT   (agent CLI killed by its timeout)
  FetchError{Stage, StatusCode, RateLimited}   (release / manifest / skill file)
    context.Canceled             -> COMMAND_INTERRUPTED
    transport failure            -> NETWORK / TIMEOUT     (no response, or a timeout/reset while reading the body)
    RateLimited                  -> RATE_LIMITED          (429, or 403 with GitHub's rate-limit headers)
    clicompat.ErrNotFound        -> MANIFEST_NOT_FOUND
    Stage == release             -> RELEASE_LOOKUP_FAILED
    JSON syntax/type error       -> MANIFEST_INVALID
    otherwise                    -> SKILLS_FETCH_FAILED
  StateError{Path}               -> STATE_INVALID         (unparsable .state.json)
  • FetchError and StateError keep the wrapped error's message, so what the user sees does not change.
  • A fetch that fails on the transport (no response, or a timeout or reset while reading the body) keeps reporting NETWORK or TIMEOUT, as before.
  • A missing manifest wraps clicompat.ErrNotFound, so clicompat.IsNotFoundError stays the one way to check for a missing ref.
  • An unreadable state file is not a StateError: it keeps its filesystem category.
  • FetchManifest reads the whole body (capped at 1 MiB, like the release lookup) before parsing it, so a connection closed cleanly mid-body (SKILLS_FETCH_FAILED; a reset reports NETWORK) is told apart from an empty or truncated manifest (MANIFEST_INVALID).
  • Changelog fragment: .nextchanges/cli/aitools-install-fetch-error-categories.md.

Evidence

Run at this head, with libs/aitools/installer/source.go and state.go temporarily set back to the parent's version (not committed):

  • Before:
    --- FAIL: TestClassifyInstallErrorCorruptStateIsStateInvalid   expected "STATE_INVALID", actual "UNCATEGORIZED"
    --- FAIL: TestClassifyRealManifestFetchErrors
        200_with_HTML / wrong_JSON_shape / empty_body / incomplete_JSON   expected "MANIFEST_INVALID",    actual "UNCATEGORIZED"
        connection_closed_mid-body / 503                                  expected "SKILLS_FETCH_FAILED", actual "UNCATEGORIZED"
    FAIL  github.com/databricks/cli/cmd/aitools
    
    After:
    ok  github.com/databricks/cli/cmd/aitools               (TestClassify*, funnel tests)
    ok  github.com/databricks/cli/libs/aitools/installer    (TestFetchLatestSkillsReleaseRef*, TestFetchErrorsCarryStage, TestLoadState*, TestRunAgentCmdReportsTimeout)
    ok  github.com/databricks/cli/acceptance                (TestAccept/cmd/aitools/telemetry/install-failure: broken-ref now SKILLS_FETCH_FAILED)
    

Merge Danger

Door: two-way

Reclassification of error_category (telemetry and --output json) plus typed wrappers; reverting restores the old categories.

Blast Radius: telemetry and the error_category field of aitools install --output json

  • Fetches that got an error response (non-OK status, unusable payload) move from UNCATEGORIZED to the categories above; fetches that fail on the transport (no response, or a timeout or reset mid-body) still report NETWORK or TIMEOUT.
  • The detail of a timed-out agent CLI is now timed out after <timeout> instead of whatever partial stderr the killed process left.
  • FetchManifest now rejects trailing data after the manifest object (json.Unmarshal instead of a streaming decoder).
  • No change to exit codes, network calls or other error messages.
  • TestLoadStateUnreadableIsNotStateError skips on Windows and as root.

This pull request and its description were written by Isaac.

@anton-107
anton-107 requested review from a team and lennartkats-db as code owners November 11, 2026 14:26
@anton-107 anton-107 added the ai-auto PRs made by AI loops & factories, label Nov 11, 2026
@anton-107
anton-107 requested review from a team as code owners November 11, 2026 14:26
@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from 491114e to 3081662 Compare November 11, 2026 14:33
@anton-107
anton-107 requested a review from a team as a code owner November 11, 2026 14:33
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from 9132948 to 405e077 Compare November 11, 2026 14:33
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Commit: 7f0dcdfbe | Workflow run

Regression test report

Selected 12 changed tests (limit=20, 0 not selected)

12 tests: regression=8 unreleased=0 coverage=4 failing=0
test branch main (8b4f052) latest (v1.26.0)
TestAccept/cmd/aitools/install-host-metadata ✅ ❌ ❌
TestAccept/cmd/aitools/list/explicit-ref ✅ ✅ ✅
TestAccept/cmd/aitools/list/latest ✅ ✅ ✅
TestAccept/cmd/aitools/list/latest-lookup-fails ✅ ✅ ✅
TestAccept/cmd/aitools/list/pinned ✅ ✅ ✅
TestAccept/cmd/aitools/telemetry/apps-init-tip ✅ ❌ ❌
TestAccept/cmd/aitools/telemetry/hint ✅ ❌ ❌
TestAccept/cmd/aitools/telemetry/install ✅ ❌ ❌
TestAccept/cmd/aitools/telemetry/install-failure ✅ ❌ ❌
TestAccept/cmd/aitools/telemetry/update-uninstall ✅ ❌ ❌
TestAccept/cmd/aitools/telemetry/workspace-unreachable ✅ ❌ ❌
TestAccept/cmd/version ✅ ❌ ❌
TestAccept/cmd/aitools/install-host-metadata: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌

main (8b4f052):

acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:42535" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1345
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1086
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:642
        	Error:      	Not equal: 
        	            	expected: "\n=== workspace unreachable: the background auth resolution prints nothing\n\n>>> [CLI] aitools install --skills-only --scope=global --agents=claude-code\nInstalling Databricks skills for Claude Code...\nUsing skills version test-ref\nFetching skills manifest...\nInstalled 1 skill.\n\n=== --debug still shows the hostmetadata line, at debug level\nDebug: [hostmetadata] failed to fetch host metadata for https://unreachable.test\n"
        	            	actual  : "\n=== workspace unreachable: the background auth resolution prints nothing\n\n>>> [CLI] aitools install --skills-only --scope=global --agents=claude-code\nWarn: [hostmetadata] failed to fetch host metadata for https://unreachable.test, will skip for 1m0s\nInstalling Databricks skills for Claude Code...\nUsing skills version test-ref\nFetching skills manifest...\nInstalled 1 skill.\n\n=== --debug still shows the hostmetadata line, at debug level\n\nExit code: 1\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -4,2 +4,3 @@
        	            	 >>> [CLI] aitools install --skills-only --scope=global --agents=claude-code
        	            	+Warn: [hostmetadata] failed to fetch host metadata for https://unreachable.test, will skip for 1m0s
        	            	 Installing Databricks skills for Claude Code...
        	            	@@ -10,3 +11,4 @@
        	            	 === --debug still shows the hostmetadata line, at debug level
        	            	-Debug: [hostmetadata] failed to fetch host metadata for https://unreachable.test
        	            	 
        	            	+Exit code: 1
        	            	+
        	Test:       	TestAccept/cmd/aitools/install-host-metadata
        	Messages:   	cmd/aitools/install-host-metadata/output.txt vs /tmp/TestAcceptcmdaitoolsinstall-host-metadata1376361951/002/output.txt

latest (v1.26.0):

acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:41557" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1345
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1086
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:642
        	Error:      	Not equal: 
        	            	expected: "\n=== workspace unreachable: the background auth resolution prints nothing\n\n>>> [CLI] aitools install --skills-only --scope=global --agents=claude-code\nInstalling Databricks skills for Claude Code...\nUsing skills version test-ref\nFetching skills manifest...\nInstalled 1 skill.\n\n=== --debug still shows the hostmetadata line, at debug level\nDebug: [hostmetadata] failed to fetch host metadata for https://unreachable.test\n"
        	            	actual  : "\n=== workspace unreachable: the background auth resolution prints nothing\n\n>>> [CLI] aitools install --skills-only --scope=global --agents=claude-code\nWarn: [hostmetadata] failed to fetch host metadata for https://unreachable.test, will skip for 1m0s\nInstalling Databricks skills for Claude Code...\nUsing skills version test-ref\nFetching skills manifest...\nInstalled 1 skill.\n\n=== --debug still shows the hostmetadata line, at debug level\n\nExit code: 1\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -4,2 +4,3 @@
        	            	 >>> [CLI] aitools install --skills-only --scope=global --agents=claude-code
        	            	+Warn: [hostmetadata] failed to fetch host metadata for https://unreachable.test, will skip for 1m0s
        	            	 Installing Databricks skills for Claude Code...
        	            	@@ -10,3 +11,4 @@
        	            	 === --debug still shows the hostmetadata line, at debug level
        	            	-Debug: [hostmetadata] failed to fetch host metadata for https://unreachable.test
        	            	 
        	            	+Exit code: 1
        	            	+
        	Test:       	TestAccept/cmd/aitools/install-host-metadata
        	Messages:   	cmd/aitools/install-host-metadata/output.txt vs /tmp/TestAcceptcmdaitoolsinstall-host-metadata1388294159/002/output.txt
TestAccept/cmd/aitools/telemetry/apps-init-tip: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌

main (8b4f052):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:42683" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:42683" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1345
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1086
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:642
        	Error:      	Not equal: 
        	            	expected: "\n=== apps init with flags prints the tip: one hint-shown event per hint printed\n\n>>> [CLI] apps init --template ./template --skip-install --name app-tip\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-tip\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n{\"command\":\"apps_init\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"INSTALL_TEXT\"}}\n{\"command\":\"apps_init\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"APPS_INIT_TIP\"}}\n\n=== plugin installed: no hint, no tip, no event\n\n>>> [CLI] apps init --template ./template --skip-install --name app-plugin\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-plugin\n  Files: 3\n\n\n>>> print_telemetry\n"
        	            	actual  : "\n=== apps init with flags prints the tip: one hint-shown event per hint printed\n\n>>> [CLI] apps init --template ./template --skip-install --name app-tip\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-tip\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no tip, no event\n\n>>> [CLI] apps init --template ./template --skip-install --name app-plugin\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-plugin\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -15,4 +15,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"apps_init","exit_code":0,"aitools_hint_event":{"hint_kind":"INSTALL_TEXT"}}
        	            	-{"command":"apps_init","exit_code":0,"aitools_hint_event":{"hint_kind":"APPS_INIT_TIP"}}
        	            	 
        	            	@@ -28,2 +26,3 @@
        	            	 
        	            	+Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.
        	            	 
        	Test:       	TestAccept/cmd/aitools/telemetry/apps-init-tip
        	Messages:   	cmd/aitools/telemetry/apps-init-tip/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryapps-init-tip3766722146/002/output.txt
    acceptance_test.go:1123: LOG.apps-init: 
    acceptance_test.go:1123: LOG.apps-init-plugin:

latest (v1.26.0):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:33209" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:33209" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1345
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1086
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:642
        	Error:      	Not equal: 
        	            	expected: "\n=== apps init with flags prints the tip: one hint-shown event per hint printed\n\n>>> [CLI] apps init --template ./template --skip-install --name app-tip\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-tip\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n{\"command\":\"apps_init\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"INSTALL_TEXT\"}}\n{\"command\":\"apps_init\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"APPS_INIT_TIP\"}}\n\n=== plugin installed: no hint, no tip, no event\n\n>>> [CLI] apps init --template ./template --skip-install --name app-plugin\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-plugin\n  Files: 3\n\n\n>>> print_telemetry\n"
        	            	actual  : "\n=== apps init with flags prints the tip: one hint-shown event per hint printed\n\n>>> [CLI] apps init --template ./template --skip-install --name app-tip\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-tip\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no tip, no event\n\n>>> [CLI] apps init --template ./template --skip-install --name app-plugin\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-plugin\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -15,4 +15,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"apps_init","exit_code":0,"aitools_hint_event":{"hint_kind":"INSTALL_TEXT"}}
        	            	-{"command":"apps_init","exit_code":0,"aitools_hint_event":{"hint_kind":"APPS_INIT_TIP"}}
        	            	 
        	            	@@ -28,2 +26,3 @@
        	            	 
        	            	+Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.
        	            	 
        	Test:       	TestAccept/cmd/aitools/telemetry/apps-init-tip
        	Messages:   	cmd/aitools/telemetry/apps-init-tip/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryapps-init-tip3310954329/002/output.txt
    acceptance_test.go:1123: LOG.apps-init: 
    acceptance_test.go:1123: LOG.apps-init-plugin:
TestAccept/cmd/aitools/telemetry/hint: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌

main (8b4f052):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:36199" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:36199" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1345
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1086
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:642
        	Error:      	Not equal: 
        	            	expected: "\n=== hint printed on an authenticated command: one hint-shown event is uploaded\n\n>>> [CLI] jobs list\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n\n>>> print_telemetry\n{\"command\":\"jobs_list\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"INSTALL_TEXT\"}}\n\n=== hint printed on a command that resolves no auth: nothing is uploaded\n\n>>> [CLI] version\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\nDatabricks CLI v[CLI_VERSION]\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no event\n\n>>> [CLI] jobs list\n\n>>> print_telemetry\n"
        	            	actual  : "\n=== hint printed on an authenticated command: one hint-shown event is uploaded\n\n>>> [CLI] jobs list\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n\n>>> print_telemetry\n\n=== hint printed on a command that resolves no auth: nothing is uploaded\n\n>>> [CLI] version\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\nDatabricks CLI v[CLI_VERSION]\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no event\n\n>>> [CLI] jobs list\n\n>>> print_telemetry\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -7,3 +7,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"jobs_list","exit_code":0,"aitools_hint_event":{"hint_kind":"INSTALL_TEXT"}}
        	            	 
        	Test:       	TestAccept/cmd/aitools/telemetry/hint
        	Messages:   	cmd/aitools/telemetry/hint/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryhint2293571044/002/output.txt

latest (v1.26.0):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:45171" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:45171" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1345
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1086
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:642
        	Error:      	Not equal: 
        	            	expected: "\n=== hint printed on an authenticated command: one hint-shown event is uploaded\n\n>>> [CLI] jobs list\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n\n>>> print_telemetry\n{\"command\":\"jobs_list\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"INSTALL_TEXT\"}}\n\n=== hint printed on a command that resolves no auth: nothing is uploaded\n\n>>> [CLI] version\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\nDatabricks CLI v[CLI_VERSION]\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no event\n\n>>> [CLI] jobs list\n\n>>> print_telemetry\n"
        	            	actual  : "\n=== hint printed on an authenticated command: one hint-shown event is uploaded\n\n>>> [CLI] jobs list\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n\n>>> print_telemetry\n\n=== hint printed on a command that resolves no auth: nothing is uploaded\n\n>>> [CLI] version\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\nDatabricks CLI v[CLI_VERSION]\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no event\n\n>>> [CLI] jobs list\n\n>>> print_telemetry\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -7,3 +7,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"jobs_list","exit_code":0,"aitools_hint_event":{"hint_kind":"INSTALL_TEXT"}}
        	            	 
        	Test:       	TestAccept/cmd/aitools/telemetry/hint
        	Messages:   	cmd/aitools/telemetry/hint/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryhint1603266831/002/output.txt
TestAccept/cmd/aitools/telemetry/install: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌

main (8b4f052):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:45931" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:45931" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: Diff:
        --- cmd/aitools/telemetry/install/output.txt
        +++ /tmp/TestAcceptcmdaitoolstelemetryinstall1879795270/002/output.txt
        @@ -8,14 +8,7 @@
         Installed 1 skill.
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SKILLS_INSTALLED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
         {"command":"aitools_install","exit_code":0,"aitools_install_event":{"agents":["CLAUDE_CODE"],"scope":"GLOBAL","error_category":"TYPE_UNSPECIFIED"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"SUCCESS","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CLAUDE_CODE"],"scope":"GLOBAL"}}
         
         === databricks-cli auth with a valid cached token: the event is uploaded
         
        @@ -26,14 +18,7 @@
         Installed 1 skill.
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SKILLS_INSTALLED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
         {"command":"aitools_install","exit_code":0,"aitools_install_event":{"agents":["CLAUDE_CODE"],"scope":"GLOBAL","error_category":"TYPE_UNSPECIFIED"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"SUCCESS","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CLAUDE_CODE"],"scope":"GLOBAL"}}
         
         === databricks-cli auth with an expired cached token: no refresh, nothing is uploaded
         
        @@ -44,6 +28,22 @@
         Installed 1 skill.
         
         >>> print_requests.py //oidc/v1/token //telemetry-ext
        +{
        +  "method": "POST",
        +  "path": "/oidc/v1/token",
        +  "raw_body": "client_id=databricks-cli&grant_type=refresh_token&refresh_token=test-refresh-token"
        +}
        +{
        +  "method": "POST",
        +  "path": "/telemetry-ext",
        +  "body": {
        +    "uploadTime": [UNIX_TIME_MILLIS],
        +    "items": [],
        +    "protoLogs": [
        +      "{\"frontend_log_event_id\":\"[UUID][0]\",\"entry\":{\"databricks_cli_log\":{\"execution_context\":{\"cmd_exec_id\":\"[UUID][1]\",\"version\":\"[CLI_VERSION]\",\"command\":\"aitools_install\",\"operating_system\":\"linux\",\"execution_time_ms\":16,\"exit_code\":0},\"aitools_install_event\":{\"agents\":[\"CLAUDE_CODE\"],\"scope\":\"GLOBAL\",\"error_category\":\"TYPE_UNSPECIFIED\"}}}}"
        +    ]
        +  }
        +}
         
         >>> gron.py [TEST_TMP_DIR]/home/.databricks/token-cache.json
        -json.tokens.test-profile.access_token = "expired-access-token";
        +json.tokens.test-profile.access_token = "oauth-token";
        
FAIL

latest (v1.26.0):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:34903" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:34903" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: Diff:
        --- cmd/aitools/telemetry/install/output.txt
        +++ /tmp/TestAcceptcmdaitoolstelemetryinstall932013446/002/output.txt
        @@ -8,14 +8,7 @@
         Installed 1 skill.
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SKILLS_INSTALLED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
         {"command":"aitools_install","exit_code":0,"aitools_install_event":{"agents":["CLAUDE_CODE"],"scope":"GLOBAL","error_category":"TYPE_UNSPECIFIED"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"SUCCESS","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CLAUDE_CODE"],"scope":"GLOBAL"}}
         
         === databricks-cli auth with a valid cached token: the event is uploaded
         
        @@ -26,14 +18,7 @@
         Installed 1 skill.
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SKILLS_INSTALLED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
         {"command":"aitools_install","exit_code":0,"aitools_install_event":{"agents":["CLAUDE_CODE"],"scope":"GLOBAL","error_category":"TYPE_UNSPECIFIED"}}
        -{"command":"aitools_install","exit_code":0,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"SUCCESS","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CLAUDE_CODE"],"scope":"GLOBAL"}}
         
         === databricks-cli auth with an expired cached token: no refresh, nothing is uploaded
         
        @@ -44,6 +28,22 @@
         Installed 1 skill.
         
         >>> print_requests.py //oidc/v1/token //telemetry-ext
        +{
        +  "method": "POST",
        +  "path": "/oidc/v1/token",
        +  "raw_body": "client_id=databricks-cli&grant_type=refresh_token&refresh_token=test-refresh-token"
        +}
        +{
        +  "method": "POST",
        +  "path": "/telemetry-ext",
        +  "body": {
        +    "uploadTime": [UNIX_TIME_MILLIS],
        +    "items": [],
        +    "protoLogs": [
        +      "{\"frontend_log_event_id\":\"[UUID][0]\",\"entry\":{\"databricks_cli_log\":{\"execution_context\":{\"cmd_exec_id\":\"[UUID][1]\",\"version\":\"[CLI_VERSION]\",\"command\":\"aitools_install\",\"operating_system\":\"linux\",\"execution_time_ms\":12,\"exit_code\":0},\"aitools_install_event\":{\"agents\":[\"CLAUDE_CODE\"],\"scope\":\"GLOBAL\",\"error_category\":\"TYPE_UNSPECIFIED\"}}}}"
        +    ]
        +  }
        +}
         
         >>> gron.py [TEST_TMP_DIR]/home/.databricks/token-cache.json
        -json.tokens.test-profile.access_token = "expired-access-token";
        +json.tokens.test-profile.access_token = "oauth-token";
TestAccept/cmd/aitools/telemetry/install-failure: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌

main (8b4f052):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:46883" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:46883" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: Diff:
        --- cmd/aitools/telemetry/install-failure/output.txt
        +++ /tmp/TestAcceptcmdaitoolstelemetryinstall-failure172075294/002/output.txt
        @@ -8,12 +8,6 @@
         Error: skill "nonexistent" not found
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"FAILURE","error_category":"SKILL_NOT_FOUND","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CURSOR"],"scope":"GLOBAL"}}
         {"command":"aitools_install","exit_code":1,"aitools_install_event":{"agents":["CURSOR"],"scope":"GLOBAL","error_category":"SKILL_NOT_FOUND"}}
         
         === a manifest the server does not have is MANIFEST_NOT_FOUND
        @@ -25,12 +19,6 @@
         Error: skills manifest at databricks-agent-skills@missing-ref: not found
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"FAILURE","error_category":"MANIFEST_NOT_FOUND","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CURSOR"],"scope":"GLOBAL"}}
         {"command":"aitools_install","exit_code":1,"aitools_install_event":{"agents":["CURSOR"],"scope":"GLOBAL","error_category":"MANIFEST_NOT_FOUND"}}
         
         === any other HTTP error status from the manifest fetch is SKILLS_FETCH_FAILED
        @@ -42,13 +30,7 @@
         Error: failed to fetch manifest: HTTP 500
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"FAILURE","error_category":"SKILLS_FETCH_FAILED","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CURSOR"],"scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_install_event":{"agents":["CURSOR"],"scope":"GLOBAL","error_category":"SKILLS_FETCH_FAILED"}}
        +{"command":"aitools_install","exit_code":1,"aitools_install_event":{"agents":["CURSOR"],"scope":"GLOBAL","error_category":"UNCATEGORIZED"}}
         
         === a named agent skipped for the scope takes its category from the agent's outcome
         
        @@ -57,10 +39,4 @@
         Error: Cursor: does not support project-scoped skills
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"PROJECT"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"PROJECT"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"PROJECT"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"FAILURE","error_category":"UNSUPPORTED_SCOPE","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CURSOR"],"scope":"PROJECT"}}
         {"command":"aitools_install","exit_code":1,"aitools_install_event":{"agents":["CURSOR"],"scope":"PROJECT","error_category":"TYPE_UNSPECIFIED","agent_results":[{"agent":"CURSOR","error_category":"UNSUPPORTED_SCOPE"}]}}

latest (v1.26.0):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:33411" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:33411" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: Diff:
        --- cmd/aitools/telemetry/install-failure/output.txt
        +++ /tmp/TestAcceptcmdaitoolstelemetryinstall-failure1624804049/002/output.txt
        @@ -8,12 +8,6 @@
         Error: skill "nonexistent" not found
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"FAILURE","error_category":"SKILL_NOT_FOUND","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CURSOR"],"scope":"GLOBAL"}}
         {"command":"aitools_install","exit_code":1,"aitools_install_event":{"agents":["CURSOR"],"scope":"GLOBAL","error_category":"SKILL_NOT_FOUND"}}
         
         === a manifest the server does not have is MANIFEST_NOT_FOUND
        @@ -25,12 +19,6 @@
         Error: skills manifest at databricks-agent-skills@missing-ref: not found
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"FAILURE","error_category":"MANIFEST_NOT_FOUND","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CURSOR"],"scope":"GLOBAL"}}
         {"command":"aitools_install","exit_code":1,"aitools_install_event":{"agents":["CURSOR"],"scope":"GLOBAL","error_category":"MANIFEST_NOT_FOUND"}}
         
         === any other HTTP error status from the manifest fetch is SKILLS_FETCH_FAILED
        @@ -42,13 +30,7 @@
         Error: failed to fetch manifest: HTTP 500
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"FAILURE","error_category":"SKILLS_FETCH_FAILED","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CURSOR"],"scope":"GLOBAL"}}
        -{"command":"aitools_install","exit_code":1,"aitools_install_event":{"agents":["CURSOR"],"scope":"GLOBAL","error_category":"SKILLS_FETCH_FAILED"}}
        +{"command":"aitools_install","exit_code":1,"aitools_install_event":{"agents":["CURSOR"],"scope":"GLOBAL","error_category":"UNCATEGORIZED"}}
         
         === a named agent skipped for the scope takes its category from the agent's outcome
         
        @@ -57,10 +39,4 @@
         Error: Cursor: does not support project-scoped skills
         
         >>> print_telemetry
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED","interactive":false,"install_mode":"SKILLS_ONLY"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"SCOPE_RESOLVED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"PROJECT"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"AGENTS_SELECTED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"PROJECT"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"STEP_COMPLETED","step":"PLAN_CONFIRMED","interactive":false,"install_mode":"SKILLS_ONLY","scope":"PROJECT"}}
        -{"command":"aitools_install","exit_code":1,"aitools_command_event":{"command":"INSTALL","stage":"FINISHED","outcome":"FAILURE","error_category":"UNSUPPORTED_SCOPE","interactive":false,"install_mode":"SKILLS_ONLY","agents":["CURSOR"],"scope":"PROJECT"}}
         {"command":"aitools_install","exit_code":1,"aitools_install_event":{"agents":["CURSOR"],"scope":"PROJECT","error_category":"TYPE_UNSPECIFIED","agent_results":[{"agent":"CURSOR","error_category":"UNSUPPORTED_SCOPE"}]}}
        
FAIL
TestAccept/cmd/aitools/telemetry/update-uninstall: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌

main (8b4f052):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:40813" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:40813" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1345
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1086
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:642
        	Error:      	Not equal: 
        	            	expected: "\n=== update: STARTED, FINISHED with SUCCESS\n\n>>> [CLI] aitools update --scope=global\nAlready up to date.\n\n>>> print_telemetry\n{\"command\":\"aitools_update\",\"exit_code\":0,\"aitools_command_event\":{\"command\":\"UPDATE\",\"stage\":\"STARTED\",\"interactive\":false}}\n{\"command\":\"aitools_update\",\"exit_code\":0,\"aitools_command_event\":{\"command\":\"UPDATE\",\"stage\":\"FINISHED\",\"outcome\":\"SUCCESS\",\"interactive\":false,\"scope\":\"GLOBAL\"}}\n\n=== uninstall: STARTED, FINISHED with SUCCESS\n\n>>> [CLI] aitools uninstall --scope=global\nUninstalled 1 skill.\n\n>>> print_telemetry\n{\"command\":\"aitools_uninstall\",\"exit_code\":0,\"aitools_command_event\":{\"command\":\"UNINSTALL\",\"stage\":\"STARTED\",\"interactive\":false}}\n{\"command\":\"aitools_uninstall\",\"exit_code\":0,\"aitools_command_event\":{\"command\":\"UNINSTALL\",\"stage\":\"FINISHED\",\"outcome\":\"SUCCESS\",\"interactive\":false,\"scope\":\"GLOBAL\"}}\n"
        	            	actual  : "\n=== update: STARTED, FINISHED with SUCCESS\n\n>>> [CLI] aitools update --scope=global\nAlready up to date.\n\n>>> print_telemetry\n\n=== uninstall: STARTED, FINISHED with SUCCESS\n\n>>> [CLI] aitools uninstall --scope=global\nUninstalled 1 skill.\n\n>>> print_telemetry\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -7,4 +7,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"aitools_update","exit_code":0,"aitools_command_event":{"command":"UPDATE","stage":"STARTED","interactive":false}}
        	            	-{"command":"aitools_update","exit_code":0,"aitools_command_event":{"command":"UPDATE","stage":"FINISHED","outcome":"SUCCESS","interactive":false,"scope":"GLOBAL"}}
        	            	 
        	            	@@ -16,4 +14,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"aitools_uninstall","exit_code":0,"aitools_command_event":{"command":"UNINSTALL","stage":"STARTED","interactive":false}}
        	            	-{"command":"aitools_uninstall","exit_code":0,"aitools_command_event":{"command":"UNINSTALL","stage":"FINISHED","outcome":"SUCCESS","interactive":false,"scope":"GLOBAL"}}
        	            	 
        	Test:       	TestAccept/cmd/aitools/telemetry/update-uninstall
        	Messages:   	cmd/aitools/telemetry/update-uninstall/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryupdate-uninstall1246129515/002/output.txt
    acceptance_test.go:1123: 
        LOG.install: Installing Databricks skills for Cursor...
        LOG.install: Using skills version test-ref
        LOG.install: Fetching skills manifest...
        LOG.install: Installed 1 skill.
    acceptance_test.go:1123: LOG.install-telemetry: {"command":"aitools_install","exit_code":0,"aitools_install_event":{"agents":["CURSOR"],"scope":"GLOBAL","error_category":"TYPE_UNSPECIFIED"}}

latest (v1.26.0):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:37813" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:37813" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1345
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1086
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:642
        	Error:      	Not equal: 
        	            	expected: "\n=== update: STARTED, FINISHED with SUCCESS\n\n>>> [CLI] aitools update --scope=global\nAlready up to date.\n\n>>> print_telemetry\n{\"command\":\"aitools_update\",\"exit_code\":0,\"aitools_command_event\":{\"command\":\"UPDATE\",\"stage\":\"STARTED\",\"interactive\":false}}\n{\"command\":\"aitools_update\",\"exit_code\":0,\"aitools_command_event\":{\"command\":\"UPDATE\",\"stage\":\"FINISHED\",\"outcome\":\"SUCCESS\",\"interactive\":false,\"scope\":\"GLOBAL\"}}\n\n=== uninstall: STARTED, FINISHED with SUCCESS\n\n>>> [CLI] aitools uninstall --scope=global\nUninstalled 1 skill.\n\n>>> print_telemetry\n{\"command\":\"aitools_uninstall\",\"exit_code\":0,\"aitools_command_event\":{\"command\":\"UNINSTALL\",\"stage\":\"STARTED\",\"interactive\":false}}\n{\"command\":\"aitools_uninstall\",\"exit_code\":0,\"aitools_command_event\":{\"command\":\"UNINSTALL\",\"stage\":\"FINISHED\",\"outcome\":\"SUCCESS\",\"interactive\":false,\"scope\":\"GLOBAL\"}}\n"
        	            	actual  : "\n=== update: STARTED, FINISHED with SUCCESS\n\n>>> [CLI] aitools update --scope=global\nAlready up to date.\n\n>>> print_telemetry\n\n=== uninstall: STARTED, FINISHED with SUCCESS\n\n>>> [CLI] aitools uninstall --scope=global\nUninstalled 1 skill.\n\n>>> print_telemetry\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -7,4 +7,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"aitools_update","exit_code":0,"aitools_command_event":{"command":"UPDATE","stage":"STARTED","interactive":false}}
        	            	-{"command":"aitools_update","exit_code":0,"aitools_command_event":{"command":"UPDATE","stage":"FINISHED","outcome":"SUCCESS","interactive":false,"scope":"GLOBAL"}}
        	            	 
        	            	@@ -16,4 +14,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"aitools_uninstall","exit_code":0,"aitools_command_event":{"command":"UNINSTALL","stage":"STARTED","interactive":false}}
        	            	-{"command":"aitools_uninstall","exit_code":0,"aitools_command_event":{"command":"UNINSTALL","stage":"FINISHED","outcome":"SUCCESS","interactive":false,"scope":"GLOBAL"}}
        	            	 
        	Test:       	TestAccept/cmd/aitools/telemetry/update-uninstall
        	Messages:   	cmd/aitools/telemetry/update-uninstall/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryupdate-uninstall1668623292/002/output.txt
    acceptance_test.go:1123: 
        LOG.install: Installing Databricks skills for Cursor...
        LOG.install: Using skills version test-ref
        LOG.install: Fetching skills manifest...
        LOG.install: Installed 1 skill.
    acceptance_test.go:1123: LOG.install-telemetry: {"command":"aitools_install","exit_code":0,"aitools_install_event":{"agents":["CURSOR"],"scope":"GLOBAL","error_category":"TYPE_UNSPECIFIED"}}
TestAccept/cmd/aitools/telemetry/workspace-unreachable: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌

main (8b4f052):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:45095" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:45095" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1345
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1086
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:642
        	Error:      	Not equal: 
        	            	expected: "\n=== install: the background auth resolution prints nothing\n\n>>> [CLI] aitools install --skills-only --scope=global --agents=cursor\nInstalling Databricks skills for Cursor...\nUsing skills version test-ref\nFetching skills manifest...\nInstalled 1 skill.\n\n=== update: the background auth resolution prints nothing\n\n>>> [CLI] aitools update --scope=global\nAlready up to date.\n\n=== uninstall: the background auth resolution prints nothing\n\n>>> [CLI] aitools uninstall --scope=global\nUninstalled 1 skill.\n"
        	            	actual  : "\n=== install: the background auth resolution prints nothing\n\n>>> [CLI] aitools install --skills-only --scope=global --agents=cursor\nWarn: [hostmetadata] failed to fetch host metadata for https://unreachable.test, will skip for 1m0s\nInstalling Databricks skills for Cursor...\nUsing skills version test-ref\nFetching skills manifest...\nInstalled 1 skill.\n\n=== update: the background auth resolution prints nothing\n\n>>> [CLI] aitools update --scope=global\nAlready up to date.\n\n=== uninstall: the background auth resolution prints nothing\n\n>>> [CLI] aitools uninstall --scope=global\nUninstalled 1 skill.\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -4,2 +4,3 @@
        	            	 >>> [CLI] aitools install --skills-only --scope=global --agents=cursor
        	            	+Warn: [hostmetadata] failed to fetch host metadata for https://unreachable.test, will skip for 1m0s
        	            	 Installing Databricks skills for Cursor...
        	Test:       	TestAccept/cmd/aitools/telemetry/workspace-unreachable
        	Messages:   	cmd/aitools/telemetry/workspace-unreachable/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryworkspace-unreachable2607473498/002/output.txt

latest (v1.26.0):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:42133" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:42133" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1345
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1086
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:642
        	Error:      	Not equal: 
        	            	expected: "\n=== install: the background auth resolution prints nothing\n\n>>> [CLI] aitools install --skills-only --scope=global --agents=cursor\nInstalling Databricks skills for Cursor...\nUsing skills version test-ref\nFetching skills manifest...\nInstalled 1 skill.\n\n=== update: the background auth resolution prints nothing\n\n>>> [CLI] aitools update --scope=global\nAlready up to date.\n\n=== uninstall: the background auth resolution prints nothing\n\n>>> [CLI] aitools uninstall --scope=global\nUninstalled 1 skill.\n"
        	            	actual  : "\n=== install: the background auth resolution prints nothing\n\n>>> [CLI] aitools install --skills-only --scope=global --agents=cursor\nWarn: [hostmetadata] failed to fetch host metadata for https://unreachable.test, will skip for 1m0s\nInstalling Databricks skills for Cursor...\nUsing skills version test-ref\nFetching skills manifest...\nInstalled 1 skill.\n\n=== update: the background auth resolution prints nothing\n\n>>> [CLI] aitools update --scope=global\nAlready up to date.\n\n=== uninstall: the background auth resolution prints nothing\n\n>>> [CLI] aitools uninstall --scope=global\nUninstalled 1 skill.\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -4,2 +4,3 @@
        	            	 >>> [CLI] aitools install --skills-only --scope=global --agents=cursor
        	            	+Warn: [hostmetadata] failed to fetch host metadata for https://unreachable.test, will skip for 1m0s
        	            	 Installing Databricks skills for Cursor...
        	Test:       	TestAccept/cmd/aitools/telemetry/workspace-unreachable
        	Messages:   	cmd/aitools/telemetry/workspace-unreachable/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryworkspace-unreachable1616350568/002/output.txt
TestAccept/cmd/version: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌

main (8b4f052):

acceptance_test.go:1345: Diff:
        --- cmd/version/output.txt
        +++ /tmp/TestAcceptcmdversion2182449636/002/output.txt
        @@ -24,7 +24,6 @@
         >>> [CLI] version --check
         Databricks CLI v[CLI_VERSION]
         This is a development build; skipping the update check.
        -Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.
         
         === version --check --output json with a coding agent prints no tip

latest (v1.26.0):

acceptance_test.go:1345: Diff:
        --- cmd/version/output.txt
        +++ /tmp/TestAcceptcmdversion573410808/002/output.txt
        @@ -8,7 +8,7 @@
         
         >>> [CLI] version --check
         Databricks CLI v[CLI_VERSION]
        -This is a development build; skipping the update check.
        +Could not reach GitHub to check for a newer version. See https://github.com/databricks/cli/releases for the latest release.
         
         === version --check --output json
         
        @@ -16,15 +16,14 @@
         {
           "current_version": "[CLI_VERSION]",
           "update_available": false,
        -  "development_build": true
        +  "check_failed": true
         }
         
         === version --check with a coding agent and no Databricks tooling
         
         >>> [CLI] version --check
         Databricks CLI v[CLI_VERSION]
        -This is a development build; skipping the update check.
        -Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.
        +Could not reach GitHub to check for a newer version. See https://github.com/databricks/cli/releases for the latest release.
         
         === version --check --output json with a coding agent prints no tip
         
        @@ -32,7 +25,7 @@
         {
           "current_version": "[CLI_VERSION]",
           "update_available": false,
        -  "development_build": true
        +  "check_failed": true
         }
         
         === version --check from Claude Code prints only the root hint
        @@ -40,4 +33,4 @@
         >>> [CLI] version --check
         Databricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install
         Databricks CLI v[CLI_VERSION]
        -This is a development build; skipping the update check.
        +Could not reach GitHub to check for a newer version. See https://github.com/databricks/cli/releases for the latest release.

@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Integration test report

Commit: 7f0dcdf

Run: 43200423266

Env 🔄​flaky 💚​RECOVERED ✅​pass 🙈​skip Time
🔄​ aws linux-2core-8gb 1 1 269 27 13:29
💚​ aws-windows-latest-4core-16gb 1 272 25 6:34
💚​ azure linux-2core-8gb 1 269 27 11:54
💚​ azure-windows-latest-4core-16gb 1 271 25 7:21
💚​ gcp linux-2core-8gb 1 270 27 15:46
💚​ gcp-windows-latest-4core-16gb 1 272 25 10:12
Test Name aws linux-2core-8gb aws-windows-latest-4core-16gb azure linux-2core-8gb azure-windows-latest-4core-16gb gcp linux-2core-8gb gcp-windows-latest-4core-16gb
💚​ TestAccept 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🔄​ TestSyncNestedSpacePlusAndHashAreEscapedSync 🔄​f ✅​p ✅​p ✅​p ✅​p ✅​p
Top 12 slowest tests (at least 2 minutes):
duration env testname
4:04 gcp-windows-latest-4core-16gb TestAccept
3:58 aws-windows-latest-4core-16gb TestAccept
3:53 azure linux-2core-8gb TestAccept
3:49 gcp linux-2core-8gb TestAccept
3:41 azure-windows-latest-4core-16gb TestAccept
3:33 aws linux-2core-8gb TestAccept
3:07 gcp linux-2core-8gb TestFilerWorkspaceFilesExtensionsReadDir
2:40 gcp-windows-latest-4core-16gb TestFsRmDirRecursively/dbfs
2:17 gcp linux-2core-8gb TestFsCpFileToDirFileNotOverwritten/dbfs_to_dbfs
2:04 gcp linux-2core-8gb TestFsCpDirToDirWithOverwriteFlag/dbfs_to_dbfs
2:04 aws linux-2core-8gb TestFilerWorkspaceFilesExtensionsStat
2:03 gcp-windows-latest-4core-16gb TestSyncIncrementalFileSync

@simonfaltum simonfaltum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Automated review from simonfaltum's review agent (pr-review skill) at 405e077. Each finding was verified against the code; simonfaltum may follow up.

Not approving yet: 1 open design question.

This PR adds typed installer errors and maps them to specific error_category values: SKILLS_FETCH_FAILED, RELEASE_LOOKUP_FAILED, RATE_LIMITED, MANIFEST_INVALID, STATE_INVALID and PLUGIN_CLI_TIMEOUT. It is well tested, lint-clean, and CI is green, and nothing found is blocking. The main points are these: transport failures (DNS errors, refused connections, timeouts) no longer report NETWORK/TIMEOUT in the released --output json contract, and the changelog doesn't mention it; and a timed-out agent CLI is still shown to the user as cli-not-on-path. The rest are small cleanups, plus one design question about reusing clicompat.HTTPStatusError.

Questions

  1. FetchError adds a typed HTTP-status error for the skills fetches, while clicompat.HTTPStatusError (libs/clicompat/clicompat.go:28) already models a non-200 response from raw.githubusercontent.com, and clicompat.IsNotFoundError understands it. Was keeping two separate types intentional? If not, would you rather extend the clicompat type (stage, rate limit, wrapped cause) and reuse it here, or keep FetchError and make IsNotFoundError recognize its 404s, so that later status checks don't have to handle both?

Comments outside the changed lines

  • [nit] cmd/aitools/categorize_test.go:151: These cases build fmt.Errorf("failed to fetch manifest: %w", <url.Error>) and expect NETWORK/TIMEOUT. FetchManifest now returns that error inside a FetchError (source.go:37), so the real error classifies as SKILLS_FETCH_FAILED. The test passes while describing behavior production no longer has. Wrap the cases in FetchError and update the expected values, or drop the "failed to fetch manifest" prefix so they test the generic stdlib path. If transport errors go back to NETWORK/TIMEOUT (see the categorize.go comment), wrapping them in FetchError{StatusCode: 0} makes them valid regression tests for that.
  • [nit] libs/aitools/installer/plugin.go:65: The doc comment says "Errors are *process.ProcessError", but on timeout runAgentCmd now returns *agentCmdTimeoutError, which wraps one. A caller that trusts the contract and type-asserts err.(*process.ProcessError) gets a failed assertion on every timeout. Update it to say a timeout returns an error wrapping the ProcessError, so callers should use errors.AsType.

Comment thread cmd/aitools/categorize.go
// The response arrived but was not a manifest: content, not transport.
case isManifestDecodeError(e):
return protos.AitoolsErrorCategoryManifestInvalid
default:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nice to have] Every installer fetch now wraps its transport error in FetchError (source.go:37, installer.go:163, installer.go:255), and fetchErrorCategory has no transport case. A DNS failure, a refused connection or the 30s client timeout now all report SKILLS_FETCH_FAILED / RELEASE_LOOKUP_FAILED, the same as a GitHub 500 (funnel_test.go:334/374 show a refused connection moving from NETWORK to SKILLS_FETCH_FAILED). In both telemetry and the --output json error_category that shipped in v1.25.0, "the user can't reach GitHub" can no longer be told apart from "GitHub failed", and a script that retries on NETWORK/TIMEOUT silently stops matching. Merge Danger mentions NETWORK but not TIMEOUT, and lists the blast radius as telemetry-only. Either classify a FetchError with StatusCode == 0 through stdlibErrorCategory(e.Err) (after the Canceled check) to keep NETWORK/TIMEOUT, or say in the changelog fragment and Merge Danger that these failures no longer report NETWORK/TIMEOUT in --output json.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in af32385: fetchErrorCategory now sends a FetchError with StatusCode == 0 (no response arrived) through stdlibErrorCategory, right after the Canceled check. So DNS failures and refused connections report NETWORK again, and client timeouts report TIMEOUT, the same as the released --output json contract. The specific categories now only apply to fetches that got a response. funnel_test.go is back to NETWORK for the refused connection. I added cases for a release lookup with no response (NETWORK), a manifest refused connection (NETWORK) and a skill file timeout (TIMEOUT). I also updated the changelog fragment and the Merge Danger section to match.

// newBlockedError builds the BlockedError for a failed agent CLI command.
func newBlockedError(agent *agents.Agent, reason string, err error) *BlockedError {
_, timedOut := errors.AsType[*agentCmdTimeoutError](err)
return &BlockedError{Agent: agent.Name, Reason: reason, Detail: stderrOf(err), TimedOut: timedOut}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nice to have] newBlockedError keeps the ReasonCLINotOnPath passed in by probePluginCLI, and BlockedError.Error() prints agent: reason: detail. A slow claude plugin --help therefore shows Skipped Claude Code: claude-code: cli-not-on-path: timed out after 20s, both in the text output (install.go:589) and in the JSON message (install.go:597). Only the telemetry category is fixed; the test comment says this used to be reported as "not on PATH", but the message still says it. A dedicated reason (e.g. ReasonCLITimedOut), set in newBlockedError when the timeout fired, would fix the message and the category together and remove the need for the TimedOut precedence rule in blockedErrorCategory.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not changed here. The later recovery-steps PR (#7480) branches on BlockedError.TimedOut and builds the user-facing line for a timeout itself ('claude plugin' timed out after 20s plus a re-run step), so the cli-not-on-path wording doesn't reach the text or JSON output once the stack lands. A new ReasonCLITimedOut would change the Reason contract that PR and #7482 depend on. I'd rather do it as a follow-up across the stack than in the middle of it.

// when no HTTP response arrived (DNS, proxy, TLS, timeout).
type FetchError struct {
Stage string
StatusCode int

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] In this PR nothing reads FetchError.StatusCode or StateError.Path, and Stage is only compared to FetchStageRelease. Yet the comments say stages exist "so the command layer can name the host the user has to reach" (lines 44-45) and that Path exists "so the user can be told which one to move aside" (line 93). Those uses arrive in #7480's recovery hints. That's fine if the stack lands together; if this PR could merge alone, reword the comments to describe what the fields do today, or move the fields into #7480.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reworded in af32385 so the comments describe what the fields do in this PR. Stages: "so a failed release lookup can be told apart from a failed download". StatusCode: it is now also read here (0 means no response, which maps to NETWORK/TIMEOUT). StateError.Path: "the state file that failed to parse".

}

func (e *agentCmdTimeoutError) Error() string {
return fmt.Sprintf("timed out after %s", e.timeout)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] agentCmdTimeoutError.Error() returns only timed out after <timeout>, and stderrOf now uses it instead of the process's stderr. With several plugin update or uninstall steps (pluginUpdateSteps, disable then uninstall), the message doesn't say which step hung. Include the subcommand in the message (e.g. plugin marketplace update timed out after 2m0s) by storing the argv, minus the binary path, on the error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not changed. #7480 renders a timeout as '<binary> plugin' <Detail>, so putting the subcommand into Detail would print it twice ('claude plugin' plugin marketplace update timed out ...). Naming the step that hung fits better there, where the user-facing line is built. I'll pick it up in that PR.

Comment thread cmd/aitools/categorize.go
}
}

func fetchErrorCategory(e *installer.FetchError) protos.AitoolsErrorCategory {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] .agents/rules/comments.md asks for a doc comment on Go functions, and this switch depends on its order: cancel, then rate limit, then not-found, then release stage, then decode. A one-line doc saying the first matching case wins, and why the release stage comes before the decode check, would stop a later reorder from silently changing categories.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in af32385: a doc comment on fetchErrorCategory saying that the first matching case wins and explaining the order (interrupted, then no response, then the release stage before the decode check).

return errors.New("signal: killed")
})

_, err := runAgentCmd(ctx, time.Millisecond, []string{"claude", "plugin", "--help"})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] err from runAgentCmd goes straight into newBlockedError. If a regression ever made runAgentCmd return nil here, stderrOf(nil) would panic on err.Error() instead of failing the test with a clear message. Add require.Error(t, err) before building the BlockedError.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added require.Error(t, err) before building the BlockedError in af32385.


func TestLoadStateCorruptReturnsStateError(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, ".state.json"), []byte("{not json"), 0o644))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] TestLoadStateCorruptReturnsStateError and TestLoadStateUnreadableIsNotStateError (lines 1596, 1601, 1609) use the literal ".state.json", but they are in package installer, which defines stateFileName (state.go:29), and state_test.go already uses it. Use the constant so a rename doesn't leave these tests checking the wrong file. These tests would also fit better in state_test.go, next to TestLoadStateCorruptJSON.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in af32385: both tests moved to state_test.go, next to TestLoadStateCorruptJSON, and they now use stateFileName.

@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from 3081662 to 9075cd6 Compare November 11, 2026 16:53
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from 405e077 to 1aa2b9f Compare November 11, 2026 16:53
@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from 9075cd6 to 4b0c705 Compare November 11, 2026 16:54
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from 1aa2b9f to bbd0a95 Compare November 11, 2026 16:54
@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from 4b0c705 to 521c720 Compare November 11, 2026 16:57
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from bbd0a95 to f18c292 Compare November 11, 2026 16:57
@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from 521c720 to dd8dd43 Compare November 11, 2026 17:28
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from f18c292 to af32385 Compare November 11, 2026 17:31
@anton-107

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All in af32385:

Question 1 (FetchError vs clicompat.HTTPStatusError). I kept FetchError, and there is still one way to check a 404: clicompat.IsNotFoundError. A manifest that isn't at the ref already wraps clicompat.ErrNotFound inside the FetchError, and IsNotFoundError checks that sentinel first. That is how FetchSkillsManifestWithFallback recognizes it for the latest-ref fallback, and the "latest manifest missing" case in TestFetchSkillsManifestWithFallback covers it. The FetchError doc comment now says to check a 404 this way, not through StatusCode. I didn't reuse HTTPStatusError: it has no wrapped cause and its message is fixed to "HTTP %d fetching manifest" for the compat manifest, so reusing it would change clicompat's own messages. clicompat also can't name an installer type without an import cycle.

Outside-diff comments.

  • categorize_test.go:151: the generic-cause cases no longer use the "failed to fetch manifest" prefix, so they test the stdlib path for errors no FetchError wraps. The FetchError transport cases are new regression tests for NETWORK/TIMEOUT.
  • plugin.go:65: the doc comment now says that on timeout the error is an *agentCmdTimeoutError wrapping the ProcessError, so callers must use errors.AsType.

The inline threads have their own replies. I declined two, the timed-out reason and the step name in the timeout message, because the user-facing timeout line is built in #7480.

@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from dd8dd43 to 270ba1d Compare November 11, 2026 19:28
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from af32385 to 2408150 Compare November 11, 2026 19:28
@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch 2 times, most recently from da3637c to f435418 Compare November 11, 2026 19:59
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from 2408150 to 5275455 Compare November 11, 2026 21:04

@simonfaltum simonfaltum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Automated review from simonfaltum's review agent (pr-review skill) at 5275455. Each finding was verified against the code; simonfaltum may follow up.

Not approving yet: 1 open design question.

This PR gives aitools install failures specific error categories. It adds typed FetchError and StateError errors and a TimedOut flag on BlockedError, and most points from the earlier round are now addressed. One nice-to-have remains: a timeout or connection reset while the response body is being read now reports SKILLS_FETCH_FAILED or RELEASE_LOOKUP_FAILED instead of TIMEOUT or NETWORK. There is also a small nit about reading the manifest without a size cap, plus one question about the long-term plan for TimedOut vs a dedicated timeout reason. CI is green and nothing is blocking.

Questions

  1. A timeout is now carried as a TimedOut flag next to Reason. A slow claude plugin --help therefore becomes Reason=cli-not-on-path, TimedOut=true: Error() still prints cli-not-on-path, and every consumer has to check the flag before switching on Reason (blockedErrorCategory here, the recovery step in #7480). You mentioned moving to a dedicated timeout reason in a follow-up across the stack, since #7480 and #7482 depend on the current Reason values. Is that follow-up tracked somewhere, such as an issue or a specific later PR in this stack? Or do you plan to keep the flag as the long-term contract? If you keep it, could the BlockedError doc say that TimedOut takes precedence over Reason?

Agreeing with existing threads

Comment thread cmd/aitools/categorize.go Outdated
// No response arrived (DNS, refused connection, proxy, TLS, client
// timeout): the user cannot reach the host, which NETWORK and TIMEOUT
// already report.
case e.StatusCode == 0:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nice to have] A timeout or connection reset while the response body is being read now loses its TIMEOUT/NETWORK category. Those FetchErrors already carry StatusCode: 200, so they skip the StatusCode == 0 case. A manifest or skill file download that hits the 30s client timeout mid-body reports SKILLS_FETCH_FAILED, a release lookup that does reports RELEASE_LOOKUP_FAILED, and a reset mid-body reports SKILLS_FETCH_FAILED. At the base commit these were TIMEOUT and NETWORK in --output json.

I reproduced it with a test server that sends the headers and part of the manifest and then stalls: TIMEOUT before, SKILLS_FETCH_FAILED now. A mid-body RST gives NETWORK before, SKILLS_FETCH_FAILED now. The changelog and Merge Danger only mention fetches that got an error response.

Suggest routing any FetchError whose cause stdlibErrorCategory reads as NETWORK or TIMEOUT through that path, e.g. case e.StatusCode == 0 || isTransportFailure(e.Err): return stdlibErrorCategory(e.Err). A clean early close (io.ErrUnexpectedEOF) still reports SKILLS_FETCH_FAILED, so the existing TestClassifyRealManifestFetchErrors cases still pass. A stalled-body case would cover the new path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4982a91. fetchErrorCategory now also routes a FetchError whose cause stdlibErrorCategory reads as NETWORK or TIMEOUT through that path (isTransportFailure), after the StatusCode == 0 case. A stalled body or a reset mid-body now reports TIMEOUT or NETWORK again, for the manifest, a skill file and the release lookup. A clean early close (io.ErrUnexpectedEOF) still reports SKILLS_FETCH_FAILED, and the existing TestClassifyRealManifestFetchErrors cases are unchanged. The new TestClassifyManifestBodyTransportFailure covers a real stalled body against a test server (TIMEOUT), a reset mid-body (NETWORK) and a release-body timeout (TIMEOUT). It fails without the fix. I updated the changelog fragment and Merge Danger to say transport failures keep NETWORK/TIMEOUT.

Comment thread libs/aitools/installer/source.go Outdated
// Read the whole body before parsing it: a connection dropped mid-body is a
// fetch failure, while a body that arrived but is empty or truncated JSON is
// an invalid manifest. A streaming decoder reports both as io.ErrUnexpectedEOF.
data, err := io.ReadAll(resp.Body)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] io.ReadAll(resp.Body) has no size cap, while the release lookup reads through io.LimitReader(resp.Body, 1<<20). With the old streaming decoder, a 200 response that isn't JSON (a captive portal, or a misconfigured DATABRICKS_SKILLS_BASE_URL mirror) failed at its first byte. Now the whole body is read into memory, for up to the 30s client timeout, before it is reported as MANIFEST_INVALID. Wrap the body in an io.LimitReader with a cap well above the real manifest's size.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4982a91: the manifest body is now read through io.LimitReader(resp.Body, maxManifestSize) with maxManifestSize = 1 << 20, the same cap as the release lookup. The real manifest is far below that. A body over the cap is cut off and reports MANIFEST_INVALID. There is a new case for it in TestClassifyRealManifestFetchErrors.

@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from 5275455 to 4982a91 Compare November 11, 2026 22:38
@anton-107

Copy link
Copy Markdown
Contributor Author

Thanks. All in 4982a91.

Question 1 (TimedOut vs a dedicated timeout reason). For this stack, TimedOut stays the contract. #7480 and #7482 build on the current Reason values, so changing them in the middle of the stack would touch every PR above this one. The BlockedError doc now says that TimedOut takes precedence over Reason and that callers check it before switching on Reason. A dedicated timeout reason, which would also fix the cli-not-on-path wording in Error(), would be a separate follow-up PR after the stack lands. No issue tracks it yet.

The two inline comments are fixed and answered in their threads: transport failures while reading the body keep NETWORK/TIMEOUT, and the manifest read is capped at 1 MiB.

@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from f435418 to 7d445a5 Compare November 11, 2026 22:58
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from 4982a91 to 09de78d Compare November 11, 2026 22:58
@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch 5 times, most recently from 3ab6a9a to 09533d4 Compare November 12, 2026 00:06
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from 09de78d to c95fa79 Compare November 12, 2026 00:11
@anton-107

Copy link
Copy Markdown
Contributor Author

The round-2 fixes cited above as 4982a91 landed, rebased onto the current base, as c95fa79.

@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from 09533d4 to 460a662 Compare November 12, 2026 00:16
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from c95fa79 to e0e7076 Compare November 12, 2026 00:16
@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from 460a662 to f1bf38a Compare November 12, 2026 00:27
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from e0e7076 to af4dc8d Compare November 12, 2026 00:27

@simonfaltum simonfaltum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Automated review from simonfaltum's review agent (pr-review skill) at af4dc8d. Each finding was verified against the code; simonfaltum may follow up.

No blocking issues found, approving. 7 non-blocking comment(s) below.

This PR gives aitools install failures specific error categories through the typed FetchError and StateError errors and a TimedOut flag on BlockedError, while transport failures keep NETWORK/TIMEOUT. Every point from the earlier rounds is either fixed or answered, CI is green on every platform, and nothing blocking remains. What's left is small: a shared constant for the 1 MiB cap, a clearer error for an oversized manifest, a stale comment on the telemetry enum, a mid-body close that lands in a different category than a reset, a simplification in fetchErrorCategory, and changelog wording. Three already-answered threads are still open; you said #7480 handles them.

Comments outside the changed lines

  • [nit] .github/PULL_REQUEST_TEMPLATE.md: .agents/skills/pr-checklist/SKILL.md asks agent-written PRs to follow .github/PULL_REQUEST_TEMPLATE.md exactly (## Changes, ## Why, ## Tests, no new sections such as ## Summary) and to keep the description short. This one uses ## Summary, ## Evidence and ## Merge Danger. Consider moving the content under the template headings and trimming the test logs.

Agreeing with existing threads

  • [nice to have] A timed-out agent CLI still says cli-not-on-path: #7475 (comment)
  • [nit] The timeout detail drops the agent CLI's stderr: #7475 (comment)
  • [nit] StateError.Path and two FetchStage values are unused in production: #7475 (comment)

Comment thread libs/aitools/installer/installer.go Outdated
}
// A response that arrived but names no usable release is still a failed
// release lookup, so these errors carry the stage too.
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&payload); err != nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] source.go:22 adds maxManifestSize = 1 << 20, and its comment says the cap "matches the release lookup's", but this line still uses the literal io.LimitReader(resp.Body, 1<<20). If either value changes, the comment in source.go becomes wrong without anyone noticing. .agents/rules/style-guide-go.md asks to reuse an existing constant in the same package instead of adding a parallel literal. Use one shared constant (e.g. maxFetchBodySize) in both places.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 57c5e35. There is one shared maxFetchBodySize = 1 << 20 in errors.go, used by both the release lookup and FetchManifest. The separate maxManifestSize and the "matches the release lookup's" comment are gone.

Comment thread libs/aitools/installer/source.go Outdated
// The cap matches the release lookup's and is far above the manifest's size,
// so a large non-manifest body (a captive portal, a wrong mirror) is not read
// whole.
data, err := io.ReadAll(io.LimitReader(resp.Body, maxManifestSize))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] A manifest body over 1 MiB is cut off without notice. io.ReadAll(io.LimitReader(...)) returns the first 1 MiB with no error, and json.Unmarshal then fails with failed to parse manifest: unexpected end of JSON input (MANIFEST_INVALID). The real manifest is about 28 KB, so this only matters if it grows a lot. If it does, the message sends people looking for a broken file rather than a size limit. Read maxManifestSize+1 bytes and return an explicit "manifest is larger than 1 MiB" error when the extra byte arrives.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not changed. An explicit "larger than 1 MiB" error would no longer be a JSON decode error, so it would move an oversized body from MANIFEST_INVALID to SKILLS_FETCH_FAILED, which is the behavior this round keeps. Keeping that category would need another marker on FetchError. The real manifest is about 28 KB, so the cap only guards against a non-manifest body. If the manifest ever grows near the cap, that is the time to add a dedicated error.

Comment thread cmd/aitools/categorize.go Outdated
return stdlibErrorCategory(e.Err)
// The response arrived but reading its body failed on the transport (a
// timeout or a reset mid-body): still NETWORK or TIMEOUT.
case isTransportFailure(e.Err):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] Body-read failures are still classified by error type. A connection that closes cleanly partway through the manifest or a skill file (io.ErrUnexpectedEOF from io.ReadAll, which is not a net.Error) reports SKILLS_FETCH_FAILED, in the same bucket as an HTTP 5xx. A reset at the same point reports NETWORK. Both are the transport failing after the response started; the connection closed mid-body case in TestClassifyRealManifestFetchErrors pins the current split. Now that FetchManifest reads and parses in separate steps, every failure of the read step is a transport failure. Consider flagging read-step failures on the FetchError and sending them through stdlibErrorCategory, treating io.ErrUnexpectedEOF as NETWORK. Otherwise, say in the changelog that a clean close mid-body reports SKILLS_FETCH_FAILED.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Kept as is, so the categories don't change after approval. A clean close mid-body (io.ErrUnexpectedEOF) still reports SKILLS_FETCH_FAILED, and a reset reports NETWORK; TestClassifyRealManifestFetchErrors pins that split. I documented it in the PR body and the commit message ("a connection closed cleanly mid-body (SKILLS_FETCH_FAILED; a reset reports NETWORK)"). I left it out of the changelog fragment to keep it short, per the fragment comment.

Comment thread cmd/aitools/categorize.go Outdated
Comment on lines +154 to +159
case e.StatusCode == 0:
return stdlibErrorCategory(e.Err)
// The response arrived but reading its body failed on the transport (a
// timeout or a reset mid-body): still NETWORK or TIMEOUT.
case isTransportFailure(e.Err):
return stdlibErrorCategory(e.Err)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] Both transport cases return stdlibErrorCategory(e.Err), and isTransportFailure calls it once more, so a transport error is classified up to three times. Computing it once removes the duplicate case and the helper. After the context.Canceled check: if c := stdlibErrorCategory(e.Err); e.StatusCode == 0 || c == protos.AitoolsErrorCategoryNetwork || c == protos.AitoolsErrorCategoryTimeout { return c }.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 57c5e35. fetchErrorCategory now calls stdlibErrorCategory(e.Err) once, after the Canceled check, and returns it when StatusCode == 0 or when it is NETWORK/TIMEOUT. The duplicate case and isTransportFailure are gone. Behavior is unchanged and the existing tests pass as they are.

Comment thread cmd/aitools/categorize.go
Comment on lines +61 to +63
// FetchError is checked before the standard library types: a response
// that arrived carries a status the net error types know nothing about.
if fetch, ok := errors.AsType[*installer.FetchError](err); ok {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] The comment above these categories in libs/telemetry/protos/aitools_install.go:55-56 says "The typed installer errors (release lookup, skills fetch) win over the generic network/timeout classification." With this PR it is the other way round: a FetchError with a transport cause reports NETWORK or TIMEOUT, and the typed categories apply only once a response arrived. Someone reading the enum would expect every failed release lookup to be RELEASE_LOOKUP_FAILED. Update that comment to match.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated in 57c5e35. The comment above the fetch categories in libs/telemetry/protos/aitools_install.go now says that a fetch failing on the transport (no response, or a timeout or reset while reading the body) reports NETWORK or TIMEOUT, and the other categories apply once a response arrived.

@@ -0,0 +1 @@
* `databricks aitools install --output json` now reports `SKILLS_FETCH_FAILED`, `RELEASE_LOOKUP_FAILED` or `RATE_LIMITED` as the `error_category` when a skills download or the latest-release lookup gets an error response (a fetch that fails on the transport, with no response or a timeout or reset while reading the body, still reports `NETWORK` or `TIMEOUT`), `MANIFEST_INVALID` for a skills manifest it cannot parse, `STATE_INVALID` for a corrupt install state file, and `PLUGIN_CLI_TIMEOUT` when a coding agent's CLI does not answer in time. ([#7475](https://github.com/databricks/cli/pull/7475))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[nit] The fragment is a single sentence of about 80 words with a nested parenthetical, and much of it describes how the classification works (transport failure vs. error response) rather than what users see. It also leaves out a visible change: a timed-out agent CLI's message now reads timed out after <d> instead of the CLI's stderr. Consider listing the new error_category values briefly, with one clause about the timeout message.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Shortened in 57c5e35. The fragment now lists the new error_category values, adds one sentence on the timed-out agent CLI message (timed out after <duration> instead of its stderr), and keeps the PR link.

@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from af4dc8d to 57c5e35 Compare November 12, 2026 09:05
@anton-107

Copy link
Copy Markdown
Contributor Author

Thanks for the approval. The nits are in 57c5e35: one shared 1 MiB constant, a single stdlibErrorCategory call in fetchErrorCategory, the corrected comment on the telemetry enum, and a shorter changelog fragment. None of them changes behavior. I left the oversized-manifest error and the clean-close category as they are, so the approved categories don't change; the reasons are in those threads.

On the PR template: I kept the current body format so all PRs in this stack share one layout, which makes them easier to read side by side. The content maps onto the template: Summary is Changes and Why, Evidence is Tests, and Merge Danger covers the impact. I removed one stale line, the claim that the change was not run on Windows or macOS (CI runs both).

@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from f1bf38a to 21231db Compare November 12, 2026 09:11
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from 57c5e35 to 8c24df0 Compare November 12, 2026 09:11
@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from 21231db to f6f7904 Compare November 12, 2026 09:19
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from 8c24df0 to 2d5e5ca Compare November 12, 2026 09:19
@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch from f6f7904 to 9f26165 Compare November 12, 2026 10:01
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from 2d5e5ca to 64a25aa Compare November 12, 2026 10:01
@anton-107
anton-107 force-pushed the aitools/plugin-ref-fallback branch 2 times, most recently from 8f2ef4e to ae31019 Compare November 12, 2026 10:24
`aitools install` failures that used to be `UNCATEGORIZED` now get a specific `error_category`. The installer returns typed errors and `classifyInstallError` maps them:

```text
classifyInstallError(err)
  BlockedError
    TimedOut                     -> PLUGIN_CLI_TIMEOUT   (agent CLI killed by its timeout)
  FetchError{Stage, StatusCode, RateLimited}   (release / manifest / skill file)
    context.Canceled             -> COMMAND_INTERRUPTED
    transport failure            -> NETWORK / TIMEOUT     (no response, or a timeout/reset while reading the body)
    RateLimited                  -> RATE_LIMITED          (429, or 403 with GitHub's rate-limit headers)
    clicompat.ErrNotFound        -> MANIFEST_NOT_FOUND
    Stage == release             -> RELEASE_LOOKUP_FAILED
    JSON syntax/type error       -> MANIFEST_INVALID
    otherwise                    -> SKILLS_FETCH_FAILED
  StateError{Path}               -> STATE_INVALID         (unparsable .state.json)
```

- `FetchError` and `StateError` keep the wrapped error's message, so what the user sees does not change.
- A fetch that fails on the transport (no response, or a timeout or reset while reading the body) keeps reporting `NETWORK` or `TIMEOUT`, as before.
- A missing manifest wraps `clicompat.ErrNotFound`, so `clicompat.IsNotFoundError` stays the one way to check for a missing ref.
- An unreadable state file is not a `StateError`: it keeps its filesystem category.
- `FetchManifest` reads the whole body (capped at 1 MiB, like the release lookup) before parsing it, so a connection closed cleanly mid-body (`SKILLS_FETCH_FAILED`; a reset reports `NETWORK`) is told apart from an empty or truncated manifest (`MANIFEST_INVALID`).
- Changelog fragment: `.nextchanges/cli/aitools-install-fetch-error-categories.md`.

Co-authored-by: Isaac <no-reply@databricks.com>
@anton-107
anton-107 force-pushed the aitools/typed-install-errors branch from 64a25aa to 7f0dcdf Compare November 12, 2026 10:25

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-auto PRs made by AI loops & factories,

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants