Skip to content

aitools: add install funnel telemetry - #7469

Open
anton-107 wants to merge 1 commit into
aitools/bound-telemetry-authfrom
aitools/install-funnel-telemetry
Open

anton-107 wants to merge 1 commit into
aitools/bound-telemetry-authfrom
aitools/install-funnel-telemetry

Conversation

@anton-107

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

Copy link
Copy Markdown
Contributor

Stacked on #7468.

Stacked on: aitools: bound best-effort telemetry auth

Summary

Every databricks aitools install run now logs an aitools_command_event funnel next to the existing aitools_install_event, so the telemetry shows where runs stop:

install RunE
  STARTED                                  (every run past flag parsing)
  validate flags, --agents names
                        --> FINISHED FAILURE INVALID_FLAGS | UNKNOWN_AGENT
  STEP FLAGS_VALIDATED
  --path dump           --> STEP SKILLS_DUMPED, FINISHED SUCCESS
                            (0 skills written: FINISHED NOTHING_TO_DO)
  scope prompt          --> FINISHED CANCELLED USER_ABORTED | FAILURE PROMPT_FAILED
  STEP SCOPE_RESOLVED
  picker                --> FINISHED CANCELLED (nothing checked)
                            no agent detected: FINISHED NOTHING_TO_DO
  STEP AGENTS_SELECTED
  confirm prompt        --> FINISHED CANCELLED (declined)
  STEP PLAN_CONFIRMED
  executePlan
    STEP SKILLS_INSTALLED   (skills group installed)
    STEP PLUGINS_INSTALLED  (at least one plugin installed)
  FINISHED SUCCESS | FAILURE <category>

Each event carries command, stage, interactive, install_mode (DEFAULT / SKILLS_ONLY / PATH) and, once known, scope. FINISHED also carries outcome, error_category and the targeted agents. Every event of a run shares the entry's cmd_exec_id. There is no calling-agent field: the upload's User-Agent already has agent/<name> and cmd-exec-id.

error_category comes from the existing classifyInstallError. Failures in flag validation, agent lookup and prompts are tagged with a category where they happen (a prompt's own failure is PROMPT_FAILED whatever its type, except a Ctrl-C abort; an error before the prompt opens keeps its own category). Error() / Unwrap() pass through, so the user still sees the same message:

 classifyInstallError(err)
   *installer.BlockedError / *installer.SkillError -> their category
+  *categorizedError (withCategory, promptError)    -> the tagged category
   stdlibErrorCategory(err)

Some runs get an explicit outcome instead of the one their return value implies:

  • Returns nil but installed nothing (e.g. the only detected agent's plugin install failed, which is a warning): FAILURE with the first failing agent's category. This includes a non-interactive run whose detected agents are all skipped for the scope (UNSUPPORTED_SCOPE). The interactive picker never offers skipped agents, so the same machine finishes NOTHING_TO_DO there.
  • Fails only because a named agent failed or was skipped (--agents): the category comes from that agent's outcome (e.g. UNSUPPORTED_SCOPE), not UNCATEGORIZED.
  • --output json failure: classifies the real error, not the ErrAlreadyPrinted sentinel.
  • --project --global and --agents ",": INVALID_FLAGS, not PROMPT_FAILED / UNKNOWN_AGENT.

Runs that end before PLAN_CONFIRMED (a flag error, a cancelled prompt, no agent detected, a --path dump) upload their events only if the background telemetry auth has already resolved; they never wait for it. Runs past PLAN_CONFIRMED keep the bounded wait.

An unknown --agents name now fails before the scope prompt instead of after it (changelog fragment .nextchanges/cli/aitools-install-agents-before-scope-prompt.md). Otherwise no change to output, prompts, exit codes or error text.

Evidence

  • Before (non-test code of this PR reverted, test files kept): acceptance cmd/aitools/telemetry/install fails; the upload has only aitools_install_event:
    --- FAIL: TestAccept/cmd/aitools/telemetry/install
      -{"aitools_command_event":{"command":"INSTALL","stage":"STARTED","interactive":false,"install_mode":"SKILLS_ONLY"}}
      -{"aitools_command_event":{..."stage":"STEP_COMPLETED","step":"FLAGS_VALIDATED"...}}
      -... SCOPE_RESOLVED, AGENTS_SELECTED, PLAN_CONFIRMED, SKILLS_INSTALLED ...
      -{"aitools_command_event":{..."stage":"FINISHED","outcome":"SUCCESS",...,"agents":["CLAUDE_CODE"],"scope":"GLOBAL"}}
    
    After: the same test passes. install-failure pins the failure paths past PLAN_CONFIRMED: missing skill, manifest 404 / 500, named agent skipped for the scope. Runs that end earlier upload only if auth already resolved, which depends on timing, so they are covered by unit tests instead.
  • Unit tests in cmd/aitools/funnel_test.go (for the funnel code in funnel.go) cover every early return and each success path (e.g. TestInstallFunnelEarlyExits, TestInstallFunnelAbortedScopePromptKeepsNamedAgents, TestInstallFunnelPathDumpSuccess, TestInstallFunnelPathDumpWithNoSkillToWrite, TestInstallFunnelPluginSuccess, TestInstallFunnelInteractiveConfirm, TestInstallFunnelDetectedPluginFailureIsNotSuccess, TestInstallFunnelSkillsFailureThroughJSON); they and TestAitoolsEventsWireKeys pass with -count=1.
  • TestInstallUnknownAgentFailsBeforeScopePrompt: with prompts supported, --agents bogus fails without showing the scope prompt (fails if resolveAgentNames moves back below the prompt).
  • The upload wait: TestInstallEarlyExitDoesNotWaitForTelemetryAuth (a flag error returns at once while auth is still resolving; fails after 10 s with the wait put back) and TestInstallPastPlanConfirmedWaitsForTelemetryAuth (auth that finishes only after the install step still reaches the upload; fails without the wait).
  • go build ./..., ./task test-exp-aitools, go test ./libs/telemetry/... ./cmd/root/..., ./task lint-q, ./task fmt-q, ./task checks, and go vet with GOOS=windows / GOOS=darwin pass.

Merge Danger

Door: two-way

Telemetry only. Reverting removes the events; nothing on disk or in the install state changes.

Blast Radius: telemetry

  • More events per aitools install upload (about eight instead of one), sent at exit and only when auth resolved. Runs that stop before the plan is confirmed (flag errors, a cancelled prompt, no agent detected) and --path dumps logged no event before, so they made no telemetry request. They now upload only if the background auth has already resolved when they end, and never wait for it, so an unreachable or slow-to-authenticate workspace adds nothing to them. When auth has resolved, the upload itself can still take up to its timeout (DATABRICKS_CLI_TELEMETRY_TIMEOUT, default 3 s) against a slow workspace. The flip side: a run that ends in milliseconds (e.g. a flag error) usually ends before auth resolves, so its events are often dropped; a run that stopped at a prompt usually had time to resolve auth. The funnel therefore under-counts the fastest early exits, and their STARTED events, relative to slower ones; the code comment next to the rule says the same. Runs past the plan confirmation keep the bounded auth wait.
  • Not covered: the funnel trusts the skills installer, which reports no per-agent delivery. So two cases still finish SUCCESS with SKILLS_INSTALLED although an agent got no skill: every skill was filtered out (experimental, or below the minimum CLI version), or the installer could not write an agent's destination (it warns and continues). --output json and aitools_install_event report these runs as installed today too. The --path dump knows how many skills it wrote, so a dump that writes none finishes NOTHING_TO_DO. A run killed by Ctrl-C outside a prompt, or rejected by flag parsing, uploads no funnel events.
  • Not tested here: interactive prompts on Windows (the funnel paths are covered by unit tests with mocked prompts and compile-checked for Windows).

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:24
@anton-107 anton-107 added the ai-auto PRs made by AI loops & factories, label Nov 11, 2026
@anton-107
anton-107 force-pushed the aitools/bound-telemetry-auth branch from 0d29efd to a15150a Compare November 11, 2026 14:32
@anton-107
anton-107 force-pushed the aitools/install-funnel-telemetry branch from 5637f13 to 21d6b41 Compare November 11, 2026 14:32
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Commit: 224174270 | Workflow run

Regression test report

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

5 tests: regression=5 unreleased=0 coverage=0 failing=0
test branch main (8b4f052) latest (v1.26.0)
TestAccept/cmd/aitools/install-host-metadata ✅ ❌ ❌
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/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:35701" ("[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-metadata3149238953/002/output.txt

latest (v1.26.0):

acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:43481" ("[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-metadata2564581388/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:33989" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:33989" ("[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-tip3065511734/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: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=== 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-tip1229271996/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:38509" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:38509" ("[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/TestAcceptcmdaitoolstelemetryhint3180399082/002/output.txt

latest (v1.26.0):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:45175" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:45175" ("[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/TestAcceptcmdaitoolstelemetryhint2865768430/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:33249" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:33249" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: Diff:
        --- cmd/aitools/telemetry/install/output.txt
        +++ /tmp/TestAcceptcmdaitoolstelemetryinstall374513363/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\":18,\"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";

latest (v1.26.0):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:44147" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:44147" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: Diff:
        --- cmd/aitools/telemetry/install/output.txt
        +++ /tmp/TestAcceptcmdaitoolstelemetryinstall804600565/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\":7,\"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:40849" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:40849" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: Diff:
        --- cmd/aitools/telemetry/install-failure/output.txt
        +++ /tmp/TestAcceptcmdaitoolstelemetryinstall-failure832100989/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 not classified yet (the error is untyped)
        @@ -42,12 +30,6 @@
         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":"UNCATEGORIZED","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":"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

latest (v1.26.0):

acceptance_test.go:1213: Substituted DATABRICKS_CLI_GITHUB_API_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:37363" ("[DATABRICKS_HOST]")
    acceptance_test.go:1213: Substituted DATABRICKS_SKILLS_BASE_URL "$DATABRICKS_HOST" -> "http://127.0.0.1:37363" ("[DATABRICKS_HOST]")
    acceptance_test.go:1345: Diff:
        --- cmd/aitools/telemetry/install-failure/output.txt
        +++ /tmp/TestAcceptcmdaitoolstelemetryinstall-failure1420514187/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 not classified yet (the error is untyped)
        @@ -42,12 +30,6 @@
         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":"UNCATEGORIZED","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":"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

@eng-dev-ecosystem-bot

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

Copy link
Copy Markdown
Collaborator

✅ Integration test report

Commit: 2241742

Run: 43200124935

Env 🔄​flaky 💚​RECOVERED ✅​pass 🙈​skip Time
💚​ aws linux-2core-8gb 1 270 20 11:19
💚​ aws-windows-latest-4core-16gb 1 272 18 8:18
💚​ azure linux-2core-8gb 1 269 20 10:18
💚​ azure-windows-latest-4core-16gb 1 271 18 7:54
💚​ gcp linux-2core-8gb 1 270 20 14:48
🔄​ gcp-windows-latest-4core-16gb 2 1 270 18 13:08
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
🔄​ TestFsCpDir ✅​p ✅​p ✅​p ✅​p ✅​p 🔄​f
🔄​ TestFsCpDir/uc-volumes_to_local ✅​p ✅​p ✅​p ✅​p ✅​p 🔄​f
Top 10 slowest tests (at least 2 minutes):
duration env testname
3:44 azure-windows-latest-4core-16gb TestAccept
3:43 aws-windows-latest-4core-16gb TestAccept
3:42 azure linux-2core-8gb TestAccept
3:40 aws linux-2core-8gb TestAccept
3:39 gcp linux-2core-8gb TestAccept
3:34 gcp-windows-latest-4core-16gb TestAccept
2:59 gcp-windows-latest-4core-16gb TestFsCpFileToFileWithOverwriteFlag/dbfs_to_dbfs
2:28 gcp linux-2core-8gb TestFsCpDirToDirFileNotOverwritten/dbfs_to_dbfs
2:08 gcp-windows-latest-4core-16gb TestFilerRecursiveDelete/dbfs
2:04 gcp linux-2core-8gb TestFilerWorkspaceFilesExtensionsStat

@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 21d6b41. Each finding was verified against the code; simonfaltum may follow up.

Not approving yet: 1 open design question.

This PR adds an aitools_command_event funnel to every aitools install run (STARTED, the steps reached, then FINISHED with an outcome and error category) and tags failures with a category where they happen. I traced every way the command can exit and found exactly one FINISHED event each time; the fields and enum values match the Universe proto, and CI is green. Nothing blocking. The main suggestion is to report "no skills delivered" the same way in --path mode and the other modes; the rest are nits on outcome semantics, small simplifications and test conventions. There is also one question about the extra upload latency for runs that stop early.

Questions

  1. Runs that stop early (a flag error, a cancelled prompt, no agent detected, a --path dump) used to upload no telemetry. With the funnel, each of them now makes an upload, so against a slow but reachable workspace a run that fails instantly can wait up to the upload timeout (3 s by default) on top of the auth wait. The Merge Danger section mentions this. Is that latency acceptable for these runs? Or would you rather use a shorter upload timeout for runs that end before PLAN_CONFIRMED, or upload their events only when auth has already resolved without waiting?

Comments outside the changed lines

  • [nit] .github/PULL_REQUEST_TEMPLATE.md: .agents/skills/pr-checklist/SKILL.md asks PR bodies to "Follow .github/PULL_REQUEST_TEMPLATE.md exactly", using ## Changes, ## Why and ## Tests and no invented sections such as ## Summary. It also asks for a concise summary. This description uses ## Summary / ## Evidence / ## Merge Danger. Map the content onto Changes / Why / Tests and trim it; the flow diagram and the "not covered" list could stay, shortened, under those headings.

Comment thread cmd/aitools/telemetry.go Outdated
Comment on lines +430 to +434
for _, o := range outcomes {
if o.status == outcomeInstalled {
return "", false
}
}

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] When every skill in the manifest is filtered out (e.g. only experimental skills, no --experimental), the outcome depends on the mode. A --path dump finishes FAILURE / UNCATEGORIZED (install.go:159). --skills-only --agents=cursor (or the default mode with a skills agent) finishes SKILLS_INSTALLED plus SUCCESS. That happens because InstallSkillsForAgents returns nil, executePlan marks the agents outcomeInstalled, and this check lets it through. The description lists this as a known gap. But the funnel now reports the same "nothing delivered" condition as both a failure and a success, and the --path side files a known, non-error cause under UNCATEGORIZED, the bucket meant to show what the classifier misses. Pick one outcome for both modes. Either carry the resolved skill count from the installer into the outcomes and report the zero case the way --path does, or, until per-agent delivery is reported, make --path match the other modes. In both cases use a specific outcome (e.g. NOTHING_TO_DO) instead of FAILURE / UNCATEGORIZED.

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 61fb77f, partly. A --path dump that writes no skill now finishes NOTHING_TO_DO instead of FAILURE / UNCATEGORIZED (TestInstallFunnelPathDumpWithNoSkillToWrite). The other modes still can't see that case: InstallSkillsForAgents returns only an error. Carrying the count would change its signature and the installSkillsForAgentsFn seam that the PRs stacked above mock, so it's not in this PR. The description lists it under "Not covered".

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.

Still only the --path part, as of 54514a6. Reporting zero skills for agent installs needs InstallSkillsForAgents to return how many skills it resolved. That changes the signature of its installSkillsForAgentsFn seam, and the PRs stacked above this one add more mocks of that seam, so the change isn't contained to this PR. For now it stays listed under "Not covered" in the description: an agent install where every skill is filtered out still finishes SUCCESS.

Comment thread cmd/aitools/install.go Outdated
if _, err := resolveScope(projectFlag, globalFlag); err != nil {
return withCategory(err, protos.AitoolsErrorCategoryInvalidFlags)
}
funnel.step(ctx, protos.AitoolsCommandStepFlagsValidated)

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] funnel.step(FLAGS_VALIDATED) runs before resolveAgentNames (line 206). So --agents , or --agents bogus logs FLAGS_VALIDATED and SCOPE_RESOLVED, then FINISHED FAILURE INVALID_FLAGS / UNKNOWN_AGENT. The step event says the flags were valid, and the final category says they were not. resolveAgentNames does not depend on the scope, so you could validate --agents before this step. Calling it there would also fail an invalid name before the interactive scope prompt instead of after it. If you'd rather keep the prompt order unchanged in this PR, note on AitoolsCommandStepFlagsValidated that --agents is checked after SCOPE_RESOLVED.

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 61fb77f. resolveAgentNames now runs before FLAGS_VALIDATED, so --agents , / --agents bogus finish STARTED -> FAILURE INVALID_FLAGS / UNKNOWN_AGENT with no step events, and fail before the scope prompt. The description notes the order change.

Comment thread cmd/aitools/telemetry.go Outdated
return "", false
}
}
return firstFailureCategory(outcomes), true

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] Suppose every detected agent is skipped for the chosen scope, e.g. --skills-only --scope=project with only Cursor detected. On the interactive path, agentChoices drops the skipped agents, selectAgents returns none, and the run finishes NOTHING_TO_DO (install.go:335). The non-interactive path (e.g. CI) keeps the skipped agents in the plan, so this function reports FAILURE / UNSUPPORTED_SCOPE. Both runs exit 0, but the funnel splits one situation into two outcomes depending on the TTY. If that is unintended, report an all-skipped plan with no named agents as NOTHING_TO_DO, or document the split.

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.

Documented in 61fb77f on nothingInstalledCategory and in the description. Keeping FAILURE / UNSUPPORTED_SCOPE for the non-interactive run is deliberate: it acted on the detected agents and set up none of them. The interactive picker never offers a skipped agent, so that run has nothing to choose and finishes NOTHING_TO_DO before the plan. #7481, further up this stack, makes the non-interactive run exit 1, so its outcome will match its exit code.

Comment thread cmd/aitools/telemetry.go Outdated
case deliverySkills:
skills = skills || o.status == outcomeInstalled
case deliveryPlugin:
plugins = plugins || o.status == outcomeInstalled

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 CLI and the Universe proto now describe three values differently. PLUGINS_INSTALLED: the proto says "The plugin install loop ran (individual agents may still have failed)", but deliverySteps emits it only when at least one plugin install succeeded. FAILURE: the proto says "The command returned an error", but the CLI also emits it for exit-0 runs (a --path dump that writes nothing, a detected agent whose plugin install failed). CANCELLED: the proto says "The user declined the confirm prompt or aborted a prompt", but the CLI also uses it for an empty picker selection, which exits 1. Anyone building dashboards from the proto will misread these runs. Update the Universe enum.proto comments to the CLI's semantics (which the Go comments in aitools_events.go already state), or change the CLI 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.

The Go comments in aitools_events.go describe the CLI's behavior; I'll align the schema descriptions separately.

Comment thread cmd/aitools/install.go Outdated
scope, err := resolveScopeWithPrompt(ctx, projectFlag, globalFlag)
if err != nil {
return err
return promptError(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] promptError wraps everything resolveScopeWithPrompt returns. But defaultPromptScopeSelection calls env.UserHomeDir and os.Getwd before any prompt opens. An interactive run in a deleted working directory therefore finishes FAILURE / PROMPT_FAILED instead of FILESYSTEM. Tag only the error from the huh ... Run() call (inside defaultPromptScopeSelection, and likewise for the picker and confirm prompts), so the earlier errors keep their stdlib classification.

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 61fb77f. promptError now wraps only the huh ... Run() error inside defaultPromptScopeSelection, defaultPromptAgentSelection and defaultPromptProceed. The call sites return errors as they are, so an env.UserHomeDir / os.Getwd failure keeps its stdlib category (FILESYSTEM for the deleted cwd).

Comment thread cmd/aitools/telemetry.go Outdated
// setScope records the resolved scope (installer.ScopeGlobal/ScopeProject).
func (f *commandFunnel) setScope(scope string) { f.scope = scopeType(scope) }

// setAgents records the targeted agents, reported on the FINISHED event.

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] setAgents wraps each agent in an agentPlanItem only so it can call agentsField. buildPlan has no side effects and runs on the next line of install.go. Build the plan first and record agentsField(plan) (e.g. funnel.setAgents(plan) taking []agentPlanItem). That drops the extra loop, and the funnel and aitools_install_event then derive agents from the same value.

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 61fb77f. The plan is built first, then funnel.setAgents(plan) records agentsField(plan), the same value aitools_install_event uses.

Comment thread cmd/aitools/install.go Outdated
}
// --project with --global is a flag error; check it here so it is not
// reported as a scope prompt failure below.
if _, err := resolveScope(projectFlag, globalFlag); 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] This resolveScope(projectFlag, globalFlag) pre-check throws its result away, and resolveScopeWithPrompt runs the same check again two lines later. That leaves its conflict error unreachable from install. Keep this tagged check and use its scope when a flag is set, calling the prompt only when neither flag is set. Then the validation and the tagging happen once.

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 61fb77f. Install calls resolveScope once, only when a scope flag is set, tags its error INVALID_FLAGS and uses its scope. resolveScopeWithPrompt(ctx) no longer takes the flags: it prompts when interactive and defaults to global otherwise. Install was its only caller.

Comment on lines +6 to +26
errcode $CLI aitools install --skills-only --path out
trace print_telemetry

title "unknown agent fails after the scope step\n"
errcode $CLI aitools install --skills-only --scope=global --agents=no-such-agent
trace print_telemetry

title "a skill missing from the manifest fails after the plan is confirmed\n"
errcode $CLI aitools install --skills-only --scope=global --agents=cursor --skills=nonexistent
trace print_telemetry

title "a manifest the server does not have is MANIFEST_NOT_FOUND\n"
errcode env DATABRICKS_SKILLS_REF=missing-ref $CLI aitools install --skills-only --scope=global --agents=cursor
trace print_telemetry

title "any other HTTP error status from the manifest fetch is not classified yet (the error is untyped)\n"
errcode env DATABRICKS_SKILLS_REF=broken-ref $CLI aitools install --skills-only --scope=global --agents=cursor
trace print_telemetry

title "a named agent skipped for the scope takes its category from the agent's outcome\n"
errcode $CLI aitools install --skills-only --scope=project --agents=cursor

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/testing.md:150 says: "Assert an expected failure with musterr, not ! cmd or a bare errcode". errcode is for tolerated failures. All six failure cases in this script must fail. Also, without trace (testing.md:148), output.txt shows each Error: ... with no >>> line naming the command. Use musterr trace $CLI ... for each case, as other scripts in the repo do.

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 61fb77f. Each case is now musterr trace $CLI .... The cases that end before PLAN_CONFIRMED (invalid flags, unknown agent, empty --path dump) are no longer in this script: those runs now upload only if auth has already resolved, so whether their events appear depends on timing. TestInstallFunnelEarlyExits and TestInstallFunnelPathDumpWithNoSkillToWrite cover them.

Comment thread cmd/aitools/funnel_test.go Outdated
func isolateAgents(t *testing.T, kv ...string) {
t.Helper()
testutil.SetAgentEnv(t, kv...)
for _, v := range []string{"CLAUDE_CONFIG_DIR", "PI_CODING_AGENT_DIR", "GEMINI_CLI_HOME", "GOOSE_PATH_ROOT", "APPDATA"} {

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] testutil.SetAgentEnv already clears the whole environment (NullEnvironment calls os.Clearenv). So this loop that blanks CLAUDE_CONFIG_DIR, GEMINI_CLI_HOME, APPDATA etc. does nothing, and the helper's comment suggests the loop is what isolates the test. Drop the loop and keep SetAgentEnv. Similarly, runInstall (line 73) takes t but never uses it. Remove the parameter, or call t.Helper().

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 61fb77f. Dropped the loop and fixed the comment. isolateAgents now only calls testutil.SetAgentEnv; I kept it as a thin wrapper because tests in the PRs stacked above call it. runInstall now calls t.Helper() (kept the parameter for the same reason).

assert.Equal(t, protos.AitoolsInstallModeSkillsOnly, e.InstallMode)
assert.False(t, e.Interactive)
}
finished := events[len(events)-1]

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] events[len(events)-1] here, and events[0] at lines 274 and 331, follow only an assert on the event sequence. If the funnel emits nothing, the test panics with index out of range instead of failing with the diff. .agents/rules/testing.md says to "Use require for preconditions that would make the rest of the test meaningless on failure", so switch those shape checks to require.Equal.

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 61fb77f. The shape checks that come before events[len(events)-1] / events[0] now use require.Equal.

@anton-107
anton-107 force-pushed the aitools/bound-telemetry-auth branch from a15150a to 86362f1 Compare November 11, 2026 16:16
@anton-107
anton-107 force-pushed the aitools/bound-telemetry-auth branch 2 times, most recently from 3147276 to 920dbc0 Compare November 11, 2026 16:56
@anton-107
anton-107 force-pushed the aitools/install-funnel-telemetry branch from 21d6b41 to 61fb77f Compare November 11, 2026 17:06
@anton-107
anton-107 force-pushed the aitools/bound-telemetry-auth branch from 920dbc0 to 95aeec8 Compare November 11, 2026 17:08
@anton-107

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Every inline comment except one has a reply in its thread.

Question 1 (upload latency for runs that stop early): changed in 61fb77f. A run that ends before PLAN_CONFIRMED (a flag error, a cancelled prompt, no agent detected, a --path dump) now uploads its funnel events only if the background telemetry auth has already resolved by then. It never waits for it (telemetryAuth.waitAtExit, set to false at the start of install RunE and back to true after PLAN_CONFIRMED). Runs past PLAN_CONFIRMED keep the bounded wait.

  • TestInstallEarlyExitDoesNotWaitForTelemetryAuth: a flag error returns at once while auth is still resolving. With the wait put back, it fails after the 10 s test budget.
  • TestInstallPastPlanConfirmedWaitsForTelemetryAuth: auth that finishes only during the install step still reaches the upload. It fails without the wait.
  • Trade-off: a run that ends in milliseconds usually ends before auth resolves, so its events are often dropped. I updated the Merge Danger bullet to say so.

PR template nit: I'd rather keep the current body format. Every PR in this stack uses the same layout, and restructuring all of them would be churn. Happy to change it if you feel strongly.

@anton-107
anton-107 force-pushed the aitools/install-funnel-telemetry branch from 61fb77f to 8fcfd3e Compare November 11, 2026 17:08
@anton-107
anton-107 force-pushed the aitools/bound-telemetry-auth branch from 95aeec8 to e503a62 Compare November 11, 2026 17:10
@anton-107
anton-107 force-pushed the aitools/install-funnel-telemetry branch 2 times, most recently from d5ef5c6 to 3b60b60 Compare November 11, 2026 19: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 3b60b60. Each finding was verified against the code; simonfaltum may follow up.

Not approving yet: 2 open design questions.

This PR adds an aitools_command_event funnel to databricks aitools install: STARTED, a STEP_COMPLETED for each step reached, and one FINISHED with the outcome and error category. It also tags failures where they happen and stops early exits from waiting for telemetry auth. The fixes from the last round are in and work as described: every exit path emits exactly one FINISHED, CI is green, and the funnel and auth tests pass locally (removing the post-plan wait makes the wait test fail 298 of 300 runs). Nothing is blocking. There are two questions about trade-offs (timing skew in early-exit data, and the bundled --agents reorder, which has no changelog entry) and a few small nits.

Questions

  1. Runs that end before the plan is confirmed now upload their events only if the background auth has already resolved. A flag error, an unknown --agents name or a non-interactive run with no agent detected ends within milliseconds, before the auth resolution (config plus a host-metadata request) can finish, so those outcomes will rarely reach the table. Slower early exits, such as a cancelled prompt, usually will. The funnel will therefore undercount the fastest drop-offs, and STARTED, relative to the slower ones. Is that skew acceptable for the dashboards you plan to build? Options: keep it and note the bias where the dashboards are defined; give early exits a short bounded wait (for example a few hundred milliseconds); or let them use the normal bounded wait.
  2. Validating --agents (and the --project/--global conflict) before the scope prompt changes what an interactive user sees: an unknown agent name now errors before the prompt instead of after it. That is a small user-visible change inside a telemetry PR, and the diff has no .nextchanges/cli fragment, which the PR checklist asks for when command or flag behavior changes. Would you rather keep it here and add a fragment, or move the reorder into its own small PR?

Agreeing with existing threads

  • [nice to have] Zero-skill agent installs finish SUCCESS while --path finishes NOTHING_TO_DO: #7469 (comment)

Comment thread cmd/aitools/install.go
Comment on lines 204 to +209
if explicit {
targetAgents, err = resolveAgentNames(ctx, agentsFlag)
if err != nil {
return err
}
} else {
}

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] With --agents, the targets are known here, but funnel.setAgents runs only after buildPlan (line 235). An interactive databricks aitools install --agents cursor with no --scope that is aborted (or fails) at the scope prompt finishes CANCELLED / USER_ABORTED (or FAILURE / PROMPT_FAILED) with an empty agents, although AitoolsCommandEvent.Agents says it is set "once the targets are known". A scratch test with the scope prompt returning huh.ErrUserAborted gives [STARTED FLAGS_VALIDATED FINISHED:CANCELLED:USER_ABORTED] with agents=[]. Record the explicit agents right after resolveAgentNames succeeds (e.g. a setAgents variant that takes []*agents.Agent), or reword the field comment to say it is set once the plan is built.

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 54514a6. Agents named via --agents are now recorded right after resolveAgentNames succeeds (funnel.setTargets), so a run aborted at the scope prompt reports them. TestInstallFunnelAbortedScopePromptKeepsNamedAgents covers it (STARTED, FLAGS_VALIDATED, FINISHED CANCELLED USER_ABORTED with agents: [CURSOR]). The plan still sets them after buildPlan for the picker path; for --agents that is the same set.

Comment on lines +3 to +5
Env.DATABRICKS_SKILLS_BASE_URL = "$DATABRICKS_HOST"
Env.DATABRICKS_CLI_GITHUB_API_URL = "$DATABRICKS_HOST"
Env.DATABRICKS_SKILLS_REF = "test-ref"

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] This parent config now sets DATABRICKS_SKILLS_BASE_URL / DATABRICKS_SKILLS_REF and serves GET /test-ref/manifest.json, but install/test.toml still sets the same two variables and serves its own manifest on the same route. The leaf stub wins (acceptance/internal/prepare_server.go:248-251), so install runs against test-stable while its siblings use databricks-test, and a change to the parent manifest never reaches install. .agents/rules/testing.md asks to put common test.toml options in the parent. Drop the duplicated env lines and stubs from install/test.toml (its output only says "Installed 1 skill."), or keep the new stubs in the new leaf directories so hint/ and apps-init-tip/ don't inherit them.

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 54514a6. Removed install/test.toml: install now uses the parent's env and manifest stubs like its siblings. Its output is unchanged ("Installed 1 skill.").

mkdir -p "${USERPROFILE:-$HOME}/.cursor"

title "install: STARTED, one event per step reached, FINISHED with SUCCESS"
trace $CLI aitools install --skills-only --scope=global --agents=cursor

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] This is the same run as the first case of install/ (--skills-only --scope=global --agents=<one agent>), whose updated output.txt already pins STARTED through FINISHED SUCCESS; only the agent differs. .agents/rules/testing.md says to extend the existing acceptance directory when coverage overlaps. Drop install-success/ and rely on install/, or fold anything extra into install/script.

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 54514a6. Dropped install-success/. The first case of install/ pins the same STARTED through FINISHED SUCCESS sequence.

Comment thread cmd/aitools/categorize.go Outdated
Comment on lines +31 to +33
if err == nil {
return 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] Every caller passes a non-nil error: string literals at the flag checks, and values inside if err != nil at install.go:172/197/422/443 and scope.go:97. So the nil branches here and in promptError (line 43) run only in TestWithCategoryKeepsMessage. AGENTS.md asks not to add speculative fallbacks or defensive nil checks for values callers always provide. Drop both nil branches and the two NoError(...(nil)) asserts, or keep them and say on the doc comment why nil must be accepted.

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 54514a6. Dropped the nil branches from withCategory and promptError and the two NoError(...(nil)) asserts. Both doc comments now say they take a non-nil error.

Comment thread cmd/aitools/telemetry.go Outdated
})
}

func (f *commandFunnel) log(ctx context.Context, e protos.AitoolsCommandEvent) {

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, starting with the name and ending with a period. This is the only new function without one. For example: // log stamps e with the run's command, interactivity, install mode and scope, and buffers it for upload.

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 54514a6, with the doc comment you suggested.

@anton-107
anton-107 force-pushed the aitools/install-funnel-telemetry branch from 3b60b60 to 54514a6 Compare November 11, 2026 23:52
@anton-107

Copy link
Copy Markdown
Contributor Author

Thanks. Every inline comment has a reply in its thread. Changes are in 54514a6.

Question 1 (early exits under-counted): I'm keeping the no-wait rule. Early exits stay instant by design, and a run that fails in milliseconds shouldn't wait seconds for telemetry. The resulting under-count of the fastest early exits (and their STARTED events) relative to slower ones is now stated in the code comment next to the rule (auth.waitAtExit = false in install RunE) and in Merge Danger.

Question 2 (--agents checked before the scope prompt): I'm keeping the reorder here. Without it, the funnel logs FLAGS_VALIDATED for a run whose flags are invalid. I added .nextchanges/cli/aitools-install-agents-before-scope-prompt.md: an unknown --agents name now fails before the scope prompt. The --project/--global conflict already failed before the prompt (the old resolveScopeWithPrompt checked it first), so that part doesn't change.

Zero-skill agent installs: answered in that thread. Only the --path part is in this PR.

@anton-107
anton-107 force-pushed the aitools/bound-telemetry-auth branch from e503a62 to 76d9ee3 Compare November 12, 2026 00:05
@anton-107
anton-107 force-pushed the aitools/install-funnel-telemetry branch from 54514a6 to f6a8dd1 Compare November 12, 2026 00:05
@anton-107
anton-107 force-pushed the aitools/bound-telemetry-auth branch from 76d9ee3 to 7e03112 Compare November 12, 2026 00:15
@anton-107
anton-107 force-pushed the aitools/install-funnel-telemetry branch from f6a8dd1 to 8ac74ab Compare November 12, 2026 00:15

@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 8ac74ab. Each finding was verified against the code; simonfaltum may follow up.

Not approving yet: 3 open design questions.

This PR adds an aitools_command_event funnel to databricks aitools install (STARTED, each step reached, one FINISHED with an outcome and error category). It also tags failures with a category where they happen, lets early exits skip the telemetry auth wait, and validates --agents before the scope prompt. Every exit path logs exactly one FINISHED, JSON mode classifies the real error, and CI is green. Nothing blocking was found. The open items are three design questions (early-exit sampling bias, the bundled --agents reorder, and two event families that define the outcome differently), a few missing funnel-sequence tests, and small test and convention nits.

Questions

  1. You've kept the no-wait rule for runs that end before the plan is confirmed and documented the under-count. That drops most fast flag errors, unknown agents and quick successful --path dumps (and their STARTED events), but keeps slow prompt cancels. Will the dashboards built on this funnel note or correct for that? Or would you consider a short bounded wait, for example a few hundred milliseconds, or waiting for a --path dump, since it does network I/O anyway?
  2. AGENTS.md asks to keep each PR focused on one change, and validating --agents before the scope prompt is a user-visible change with its own changelog entry. Would you land it as a small PR below this one, with a test that the scope prompt isn't shown for an unknown name, or do you prefer to keep it here?
  3. The same run now gets two outcome definitions. A named agent skipped for the scope reports aitools_install_event.error_category = TYPE_UNSPECIFIED with agent_results, while the funnel says FINISHED FAILURE/UNSUPPORTED_SCOPE, and an exit-0 run that installed nothing is FAILURE only in the funnel. The outcome logic also lives in three places (the funnel switch, logInstallEvent, buildInstallOutput). Did you consider computing one run result after executePlan for all three? Alternatively, aitools_install_event could be logged on every run, as its doc says, with install_mode and a last-step field instead of a separate event family, since all funnel rows upload together at exit without timestamps.

Agreeing with existing threads

  • [nice to have] An agent install where every skill is filtered out finishes SUCCESS, while --path finishes NOTHING_TO_DO: #7469 (comment)
  • [nit] PR description does not follow the repo PR template: #7469 (review)

Comment thread cmd/aitools/install.go
funnel.finishWith(ctx, protos.AitoolsCommandOutcomeNothingToDo, protos.AitoolsErrorCategoryUnspecified)
return nil
}
funnel.step(ctx, protos.AitoolsCommandStepSkillsDumped)

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] No test asserts the sequence for a successful --path dump (FLAGS_VALIDATED, SKILLS_DUMPED, FINISHED SUCCESS). The same goes for a successful plugin install (PLUGINS_INSTALLED is checked only through deliverySteps) and for an accepted interactive confirm (TestInstallFunnelInteractiveCancel has no 'confirmed' row). If one of these branches stopped logging its step or logged the wrong outcome, no test would fail. Add a success case next to TestInstallFunnelPathDumpWithNoSkillToWrite with a non-experimental skill whose SKILL.md is served, a plugin-install run using setupPluginMock, and a confirmed row in TestInstallFunnelInteractiveCancel.

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 a8819ec. Added TestInstallFunnelPathDumpSuccess (local server with a stable skill and its SKILL.md: FLAGS_VALIDATED, SKILLS_DUMPED, FINISHED SUCCESS), TestInstallFunnelPluginSuccess (setupPluginMock, --agents codex: ... PLAN_CONFIRMED, PLUGINS_INSTALLED, FINISHED SUCCESS), and a confirmed row in the interactive test, now TestInstallFunnelInteractiveConfirm (PLAN_CONFIRMED, SKILLS_INSTALLED, FINISHED SUCCESS). Dropping the SKILLS_DUMPED or PLUGINS_INSTALLED step makes the matching test fail.

Comment thread cmd/aitools/install.go
opts.Scope = scope

// Resolve the agents to act on.
// Validate --agents with the other flags, before the scope prompt.

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] The .nextchanges fragment says an unknown --agents name now fails before the scope prompt. The only tests of an unknown name (TestInstallFunnelEarlyExits, TestInstallUnknownAgentErrors) run with cmdio.MockDiscard, where the prompt is never shown, so moving resolveAgentNames back below the prompt would not fail any test. Add a test with prompts supported, --agents bogus and a promptScopeSelection stub that fails the test if it is called.

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 a8819ec. TestInstallUnknownAgentFailsBeforeScopePrompt runs with prompts supported, --agents bogus and a promptScopeSelection stub that fails the test if called. It fails when resolveAgentNames is moved back below the prompt.

Comment thread cmd/aitools/telemetry_auth_test.go Outdated
start := time.Now()
require.ErrorContains(t, cmd.Execute(), "cannot use --skills-only with --path")
assert.Less(t, time.Since(start), 5*time.Second)
assert.False(t, cmdctx.HasConfigUsed(cmd.Context()), "an unfinished resolution leaves no config, so nothing uploads")

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 blocked resolution is released only at test cleanup and has a 10 s budget. So with the wait put back, p.wait() times out and still leaves no config, and assert.False(t, cmdctx.HasConfigUsed(...)) passes either way. Only the < 5s timing check on the line above catches the regression. Drop this assertion, or reword its message so it doesn't read as a second check.

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 a8819ec. Dropped that assertion; the < 5s check is the one that catches the regression.

Comment thread cmd/aitools/telemetry.go Outdated
return protos.AitoolsInstallScopeUnspecified
}
}

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/testing.md says each source file should have a corresponding test file. The funnel code (commandFunnel, installModeType, deliverySteps, nothingInstalledCategory, firstFailureCategory) is in telemetry.go, but its tests are in the new funnel_test.go. Move the funnel code into cmd/aitools/funnel.go, or move the tests into telemetry_test.go.

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 a8819ec. Moved the funnel code (commandFunnel, installModeType, deliverySteps, nothingInstalledCategory, firstFailureCategory) unchanged from telemetry.go into cmd/aitools/funnel.go, next to funnel_test.go.

Comment thread cmd/aitools/telemetry.go Outdated
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
pending = startTelemetryAuth(cmd, args)
auth.pending = startTelemetryAuth(cmd, args)
auth.waitAtExit = true

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] Install is the only command that uses withTelemetryAuth, and its RunE sets auth.waitAtExit = false before doing anything (install.go:129), so the PreRunE assignment and the field comment's 'True at the start of every run' never take effect. Drop the PreRunE assignment and default to no-wait, so install only sets it to true at PLAN_CONFIRMED, or note why other commands need the default.

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 a8819ec. Dropped the PreRunE assignment. waitAtExit now defaults to false (no wait), and install sets it to true only at PLAN_CONFIRMED. The comment about the early-exit bias moved next to that line. withTelemetryAuth's own tests ask for the wait explicitly.

Comment thread cmd/aitools/funnel_test.go Outdated
}, funnelShape(funnelEvents(ctx)))
}

func TestInstallFunnelPathDump(t *testing.T) {

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] isolateAgents clears the whole environment, and the comment on the next test says Windows sockets need SystemRoot. On Windows the dial to 127.0.0.1:1 therefore fails during Winsock initialisation, not with connection refused. That is still a *net.OpError, so the test passes for the wrong reason. Set only HOME/USERPROFILE as TestInstallFunnelPathDumpWithNoSkillToWrite does, or keep SystemRoot.

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 a8819ec. TestInstallFunnelPathDump no longer clears the environment. It shares a setupPathDump helper with the other --path tests that sets only a temp HOME/USERPROFILE and the skills env, and stubs the telemetry auth. SystemRoot stays set, so the dial to 127.0.0.1:1 reaches the connect step on Windows too.

trace print_telemetry

title "a manifest the server does not have is MANIFEST_NOT_FOUND"
musterr trace env DATABRICKS_SKILLS_REF=missing-ref $CLI aitools install --skills-only --scope=global --agents=cursor

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/testing.md documents that trace exports leading KEY=value arguments for the command, and that form is the common one in the repo. Use musterr trace DATABRICKS_SKILLS_REF=missing-ref $CLI ... here and on line 19.

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 a8819ec. Both cases now use musterr trace DATABRICKS_SKILLS_REF=... $CLI ....

@anton-107
anton-107 force-pushed the aitools/install-funnel-telemetry branch from 8ac74ab to a8819ec Compare November 12, 2026 09:58
@anton-107

Copy link
Copy Markdown
Contributor Author

Thanks. Every inline comment has a reply in its thread. Changes are in a8819ec.

Question 1 (early-exit sampling): I'm keeping the no-wait rule as documented. A run that ends before the plan is confirmed never waits for telemetry auth, so it stays instant. The resulting bias is stated in the comment next to the rule (where install sets auth.waitAtExit = true at PLAN_CONFIRMED) and in Merge Danger. A short bounded wait for early exits, or a wait for --path dumps, can be added later without a schema change.

Question 2 (--agents reorder): I'm keeping it in this PR. Without it the funnel would log FLAGS_VALIDATED for a run whose flags are invalid. I added the test you asked for: TestInstallUnknownAgentFailsBeforeScopePrompt fails if the scope prompt is shown for an unknown name.

Question 3 (one run result): Computing one run result after executePlan, shared by the funnel, aitools_install_event and the --output json output, is a reasonable follow-up. I'd rather not do it here: it would change what aitools_install_event and the JSON output report today. This PR makes no event or schema changes beyond adding the funnel.

Every skill filtered out in an agent install: same answer as in that thread. It needs InstallSkillsForAgents to return how many skills it resolved, which changes the signature of the installSkillsForAgentsFn seam that the PRs stacked above mock, so it isn't contained to this PR.

PR template: I'm keeping the stack's single body format, as before.

@anton-107
anton-107 force-pushed the aitools/bound-telemetry-auth branch from 7e03112 to d25f879 Compare November 12, 2026 10:14
@anton-107
anton-107 force-pushed the aitools/install-funnel-telemetry branch from a8819ec to 9f73e89 Compare November 12, 2026 10:14
Every `databricks aitools install` run now logs an `aitools_command_event` funnel next to the existing `aitools_install_event`, so the telemetry shows where runs stop:

```text
install RunE
  STARTED                                  (every run past flag parsing)
  validate flags, --agents names
                        --> FINISHED FAILURE INVALID_FLAGS | UNKNOWN_AGENT
  STEP FLAGS_VALIDATED
  --path dump           --> STEP SKILLS_DUMPED, FINISHED SUCCESS
                            (0 skills written: FINISHED NOTHING_TO_DO)
  scope prompt          --> FINISHED CANCELLED USER_ABORTED | FAILURE PROMPT_FAILED
  STEP SCOPE_RESOLVED
  picker                --> FINISHED CANCELLED (nothing checked)
                            no agent detected: FINISHED NOTHING_TO_DO
  STEP AGENTS_SELECTED
  confirm prompt        --> FINISHED CANCELLED (declined)
  STEP PLAN_CONFIRMED
  executePlan
    STEP SKILLS_INSTALLED   (skills group installed)
    STEP PLUGINS_INSTALLED  (at least one plugin installed)
  FINISHED SUCCESS | FAILURE <category>
```

Each event carries `command`, `stage`, `interactive`, `install_mode` (DEFAULT / SKILLS_ONLY / PATH) and, once known, `scope`. FINISHED also carries `outcome`, `error_category` and the targeted `agents`. Every event of a run shares the entry's `cmd_exec_id`. There is no calling-agent field: the upload's User-Agent already has `agent/<name>` and `cmd-exec-id`.

`error_category` comes from the existing `classifyInstallError`. Failures in flag validation, agent lookup and prompts are tagged with a category where they happen (a prompt's own failure is PROMPT_FAILED whatever its type, except a Ctrl-C abort; an error before the prompt opens keeps its own category). `Error()` / `Unwrap()` pass through, so the user still sees the same message:

```diff
 classifyInstallError(err)
   *installer.BlockedError / *installer.SkillError -> their category
+  *categorizedError (withCategory, promptError)    -> the tagged category
   stdlibErrorCategory(err)
```

Some runs get an explicit outcome instead of the one their return value implies:
- Returns nil but installed nothing (e.g. the only detected agent's plugin install failed, which is a warning): FAILURE with the first failing agent's category.
- Fails only because a named agent failed or was skipped (`--agents`): the category comes from that agent's outcome (e.g. UNSUPPORTED_SCOPE), not UNCATEGORIZED.
- `--output json` failure: classifies the real error, not the `ErrAlreadyPrinted` sentinel.
- `--project --global` and `--agents ","`: INVALID_FLAGS, not PROMPT_FAILED / UNKNOWN_AGENT.

Runs that end before PLAN_CONFIRMED (a flag error, a cancelled prompt, no agent detected, a `--path` dump) upload their events only if the background telemetry auth has already resolved; they never wait for it, so they stay instant. The funnel therefore under-counts the fastest early exits relative to slower ones. Runs past PLAN_CONFIRMED keep the bounded wait.

An unknown `--agents` name now fails before the scope prompt instead of after it (changelog fragment `.nextchanges/cli/aitools-install-agents-before-scope-prompt.md`). Otherwise no change to output, prompts, exit codes or error text.

Co-authored-by: Isaac <no-reply@databricks.com>
@anton-107
anton-107 force-pushed the aitools/bound-telemetry-auth branch from d25f879 to fa4413b Compare November 12, 2026 10:23
@anton-107
anton-107 force-pushed the aitools/install-funnel-telemetry branch from 9f73e89 to 2241742 Compare November 12, 2026 10:23

@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.

🤖 Approval from simonfaltum's review agent at 2241742, on Simon's decision.

The design questions from the earlier automated reviews no longer block this PR: your answers are accepted, and any still-open ones are non-blocking. The changes since the last automated review (8ac74ab → 2241742) address its comments.

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