Repository navigation
Conversation
54c4bc5 to
75d4a60
Compare
5afe64b to
d5fb33f
Compare
|
Commit: Regression test reportSelected 16 changed tests (limit=20, 0 not selected) 16 tests: regression=12 unreleased=0 coverage=4 failing=0
TestAccept/cmd/aitools/install-host-metadata: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌main (8b4f052): latest (v1.26.0): TestAccept/cmd/aitools/telemetry/apps-init-tip: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌main (8b4f052): latest (v1.26.0): TestAccept/cmd/aitools/telemetry/hint: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌main (8b4f052): latest (v1.26.0): TestAccept/cmd/aitools/telemetry/install: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌main (8b4f052): latest (v1.26.0): TestAccept/cmd/aitools/telemetry/install-failure: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌main (8b4f052): latest (v1.26.0): TestAccept/cmd/aitools/telemetry/update-uninstall: branch ✅ | main (8b4f052) ❌ | latest (v1.26.0) ❌main (8b4f052): latest (v1.26.0): 6 more failure details omitted, see the full report in the run. |
✅ Integration test reportCommit: eae0662
Top 9 slowest tests (at least 2 minutes):
|
75d4a60 to
7d9546e
Compare
d5fb33f to
6dfcf53
Compare
7d9546e to
a2b82bc
Compare
6dfcf53 to
0de8029
Compare
a2b82bc to
0758e7e
Compare
0de8029 to
3e2f9ef
Compare
0758e7e to
3f574d9
Compare
simonfaltum
left a comment
There was a problem hiding this comment.
🤖 Automated review from simonfaltum's review agent (pr-review skill) at 3e2f9ef. Each finding was verified against the code; simonfaltum may follow up.
Not approving yet: 2 blocking issue(s), tagged [blocking]; 3 open design questions.
This PR makes databricks aitools install install skill files for a detected agent whose CLI cannot run, keep an existing plugin instead of adding duplicate skills, and drop stale plugin records after a fallback. The approach is reasonable, and CI passes. Two things block merging. First, the branch is built on an older revision of #7481, so it conflicts with its base and the kept-plugin logic has to be redone on top of anyInstalled/errNoAgentSetUp. Second, fallback agents are installed in a separate skills batch, so they keep stale skill copies after an upgrade. The other comments cover edge cases (kept-path cleanup, unreadable manifest, update warnings) and doc, test and description tidy-ups; three questions on the telemetry and fallback design are open.
Questions
- When a detected agent keeps its plugin and another agent's install fails, the run exits 0 but the funnel records FAILURE. An installed agent plus the same failure records SUCCESS. The current #7481 removed the exit-0-but-FAILURE branch, and per-agent results for kept or fallback agents carry the same CLI_NOT_ON_PATH entry as a real failure. Should kept be treated like installed (SUCCESS, or NOTHING_TO_DO when nothing else happened), or is the difference intended? If it is intended, could the per-agent telemetry distinguish 'kept' and 'fell back to skills' from 'failed'?
- The fallback is decided at run time inside executePlan. That is why the picker label, plan summary and outcomes disagree, and why fallback agents need a second skills install. Would deciding it at plan time in planItemFor work instead, for non-explicit plugin agents whose CLI is missing (StateInstalledCLIMissing)? That puts them in the single skills batch and keeps the summary accurate; the run-time path would only be needed when the binary is present but 'plugin --help' fails. Or is there a reason to keep it at run time?
- A global-scope fallback ignores a project or local Claude plugin that applies to the current directory, so that project ends up with both the plugin and global raw skills. Is accepting duplicates in that project the intended trade-off? The alternatives are skipping the fallback, or printing a warning when the cwd has a project-scope plugin.
Comments outside the changed lines
- [nit]
cmd/aitools/install.go:673: IfrecordPluginInstallsFnfails, executePlan returns before the fallback block. The fallback agents get nothing, even though "Installing skill files instead" was already printed for them. Run the fallback before the early return, or fold it into a single skills batch. - [nit]
.github/PULL_REQUEST_TEMPLATE.md: The repo's pr-checklist asks for the template sections## Changes,## Whyand## Tests, with no invented sections, and an agent disclosure as a single italic line. The description uses## Summary,## Evidenceand## Merge Danger. Its claim "No extra network calls beyond the existing skills manifest fetch" is also inaccurate: the fallback fetches the manifest a second time. Please restructure the description and correct that claim.
| case runErr == nil: | ||
| if category, ok := nothingInstalledCategory(outcomes); ok { | ||
| funnel.finishWith(ctx, protos.AitoolsCommandOutcomeFailure, category) | ||
| } else if onlyKeptPlugins(outcomes) { |
There was a problem hiding this comment.
[blocking] This branch sits on an older revision of #7481 (0758e7e). #7481 has since been force-pushed to 3f574d9, the base. GitHub reports this PR as not mergeable, and even a rebase of just this PR's commit conflicts in install.go, telemetry.go and funnel_test.go. The current #7481 removed nothingInstalledCategory, newNothingInstalledError and nothingInstalledError, and runErr == nil now always records SUCCESS. The kept-plugin logic here is written against those removed helpers. If the conflicts are resolved by taking the base's anyInstalled (which only checks outcomeInstalled), a kept-only run returns errNoAgentSetUp and exits 1. If the base's case runErr == nil is taken, the NOTHING_TO_DO outcome is lost. Please rebase onto the current aitools/nothing-installed-fails, count outcomeKept in anyInstalled, and redo the kept-plugin funnel handling on top of the base's runErr == nil branch.
There was a problem hiding this comment.
Rebased onto the current aitools/nothing-installed-fails (46180b9) in 7137e09. anyInstalled now counts outcomeKept, so a run whose only agent kept its plugin exits 0. The kept-plugin funnel handling sits in the base's runErr == nil branch through keptPluginsFinish: NOTHING_TO_DO when the run only kept plugins, FAILURE with the failed agent's category when it only kept plugins next to a failed install, SUCCESS when anything was installed. These outcomes are the same as in the previous revision.
| if !quiet { | ||
| installer.PrintInstallingFor(ctx, fallbackAgents) | ||
| } | ||
| if err := installSkillsForAgentsFn(ctx, src, fallbackAgents, opts); err != nil { |
There was a problem hiding this comment.
[blocking] Fallback agents are installed by a second installSkillsForAgentsFn call. At global scope a one-agent batch copies files instead of symlinking (useSymlinks = len(detectedAgents) > 1), so Cursor and Claude Code each get their own copy. On a later install with a newer skill version, the Cursor batch updates the canonical files and sets state.Skills to the new version. The fallback batch then sees a matching version and an existing Claude copy (allAgentsHaveSkill only checks that the copy exists), so it skips Claude. The command succeeds while Claude keeps the old files, on every upgrade, because the skills batch always runs first. Two batches also resolve the ref and fetch the manifest twice and print the install banner twice, as the new acceptance output shows, which contradicts the description's "no extra network calls". Please install the fallback agents in the same InstallSkillsForAgents call as the other skills agents: run the plugin loop first, or decide the fallback at plan time.
There was a problem hiding this comment.
Fixed in 7137e09. The fallback is decided in planItemFor, so these agents are deliverySkills items in the run's single InstallSkillsForAgents call: symlinks when there are several agents, one ref lookup, one manifest fetch, one banner (see the acceptance output: "Installing Databricks skills for Claude Code, Cursor..."). The second batch is gone.
| cmdio.LogString(ctx, fmt.Sprintf("Installing databricks plugin for %s...", it.agent.DisplayName)) | ||
| } | ||
| rec, err := installPluginForAgentFn(ctx, it.agent, it.scope, ref) | ||
| if err != nil && fallsBackToSkills(it, err) && pluginAlreadyInstalledFn(ctx, it.agent, opts.Scope) { |
There was a problem hiding this comment.
[nice to have] The kept branch exists to avoid duplicate skills, but it never calls cleanupLegacyFn, so raw skills written by an earlier fallback stay next to the plugin. Scenario: run 1 has no claude on PATH, falls back, and drops the plugin record. The user then installs the plugin through Claude directly. Run 2, again without claude on PATH, keeps the plugin and leaves ~/.claude/skills alone, so Claude lists every skill twice. With no record, aitools update keeps refreshing the raw files. Please call cleanupLegacyFn(ctx, it.agent, opts.Scope) in the kept branch too, as the plugin-install path does.
There was a problem hiding this comment.
Done in 7137e09: the kept path calls cleanupLegacyFn(ctx, it.agent, opts.Scope) and logs a failure at debug level, as the plugin path does. The acceptance test now runs your scenario: the first run falls back, then a Claude manifest with the plugin appears, and the second run keeps the plugin and removes Claude Code's skill files.
| project = cwd | ||
| } | ||
| } | ||
| return a.DatabricksPluginActiveIn(ctx, project) |
There was a problem hiding this comment.
[nice to have] claudePluginInstalls returns (nil, false) on any read or parse error, and DatabricksPluginActiveIn discards the ok. So pluginAlreadyInstalled returns false, the run falls back, and ForgetPluginInstalls removes a valid claude-code plugin record. If installed_plugins.json is mid-write or its schema changes, every non-interactive install without claude on PATH writes raw skills next to the installed plugin and drops the record. Please tell "no manifest file" apart from "manifest present but unreadable". In the unreadable case, fall back to the CLI state record, or keep the plugin.
There was a problem hiding this comment.
Done in 7137e09. The manifest reader now returns an error when installed_plugins.json exists but cannot be read or parsed (a missing file is still "not installed"), and DatabricksPluginActiveIn returns it. pluginAlreadyInstalled then uses the CLI's state record for the scope, so an unreadable manifest no longer drops a valid record. DatabricksTooling keeps treating a read error as not installed.
| for _, a := range fallbackAgents { | ||
| outcomes = append(outcomes, agentOutcome{agent: a, delivery: deliverySkills, status: outcomeInstalled}) | ||
| } | ||
| // These agents have no plugin (pluginAlreadyInstalled said so), so a |
There was a problem hiding this comment.
[nice to have] A fallback agent ends up with raw skills and no plugin record, which makes it a plugin-migration candidate in update (pluginMigrationCandidatesForScope). Every aitools update then calls the plugin install, fails, and prints Skipped Claude Code: claude-code: cli-not-on-path: … (the raw BlockedError). Before this PR only --skills-only users reached that state; now desktop and IDE users reach it by default. Consider skipping the migration attempt when the CLI is not on PATH, or printing the same friendly reason and recovery that install prints.
There was a problem hiding this comment.
Done in 7137e09: pluginMigrationCandidatesForScope skips an agent whose CLI is not on PATH, matching install, so aitools update keeps refreshing its raw skills without a failing plugin install on every run. Covered by TestPluginMigrationCandidatesNeedTheCLI.
| return false | ||
| } | ||
| blocked, ok := errors.AsType[*installer.BlockedError](err) | ||
| return ok && blocked.Reason == installer.ReasonCLINotOnPath && !blocked.TimedOut |
There was a problem hiding this comment.
[nit] probePluginCLI returns ReasonCLINotOnPath both when the binary is missing and when it is on PATH but plugin --help fails, for example an old CLI or a probe false negative. fallsBackToSkills doesn't tell these apart, so an on-PATH CLI that is too old now gets raw skills instead of the "update your CLI" failure. The doc comment and PR description both say "not on PATH". Either check !it.agent.HasBinary(ctx) or update the comment and description.
There was a problem hiding this comment.
Fixed in 7137e09: the fallback now checks !a.HasBinary(ctx) when the run is planned, so only a missing binary falls back. A CLI on PATH whose plugin --help fails (too old, or a probe false negative) gets the "Install or update the ... CLI" failure, as before this PR. The description says so.
|
|
||
| // A kept plugin does not hide another detected agent's failed install: the | ||
| // funnel reports the failure with that agent's category. | ||
| func TestInstallFunnelKeptPluginWithAFailedInstallIsFailure(t *testing.T) { |
There was a problem hiding this comment.
[nit] TestInstallFunnelKeptPluginIsNothingToDo and TestInstallFunnelKeptPluginWithAFailedInstallIsFailure repeat the setup of TestInstallFunnelDetectedPluginFailureIsNotSuccess. They differ only in the PATH binaries, the installer stub and the expected funnel shape. The same goes for TestExecutePlanFallsBackToSkillsWhenCLIIsMissing and TestExecutePlanKeepsAnInstalledPluginInsteadOfAddingSkills in install_test.go. .agents/rules/testing.md asks for table-driven tests for similar cases.
There was a problem hiding this comment.
Done in 7137e09: TestInstallFunnelDetectedPluginAgents is one table (the only install failed, skill files instead, plugin kept, plugin kept next to a failed install), and TestExecutePlanWhenTheCLIIsMissing covers the fallback and kept cases in one table.
| }) | ||
| continue | ||
| } | ||
| if err != nil && fallsBackToSkills(it, err) { |
There was a problem hiding this comment.
[nit] err != nil && fallsBackToSkills(it, err) is checked at line 609 and again here, followed by a third if err != nil branch for the same decision. One switch, or a single if with an inner already-installed / not-installed branch, would read more clearly (style-guide-go prefers switch for one decision).
There was a problem hiding this comment.
No longer applies in 7137e09: with the decision in planItemFor, the plugin loop in executePlan is back to its single if err != nil branch, and planItemFor makes the decision in one switch (scope unsupported, CLI missing, plugin).
| // from the state file for the given CLI scope, for agents that no longer have | ||
| // the plugin. Agents without a record, or a scope without state, are left as | ||
| // they are. | ||
| func ForgetPluginInstalls(ctx context.Context, cliScope string, agentNames []string) error { |
There was a problem hiding this comment.
[nit] ForgetPluginInstalls has no test in plugin_test.go. It is only exercised indirectly from cmd/aitools, so its no-state and no-record paths are untested. fallsBackToSkills also has no direct test: nothing checks that a TimedOut CLI_NOT_ON_PATH error or an explicit agent does not fall back. The kept notice (pluginKeptText) has no acceptance coverage either. Please add a small table test for fallsBackToSkills in recovery_test.go and a ForgetPluginInstalls test in plugin_test.go.
There was a problem hiding this comment.
Added in 7137e09: TestForgetPluginInstalls in plugin_test.go (no state, an agent without a record, a recorded agent; it also checks PluginRecorded per scope). fallsBackToSkills is gone; the plan decision that replaces it is covered by the table test TestPlanItemForAgentWithoutCLI (CLI on PATH, CLI missing, missing with the plugin, named via --agents, scope unsupported). A timeout can no longer lead to a fallback, because planning does not run the CLI. The kept notice is now in the acceptance output.
| @@ -0,0 +1 @@ | |||
| * `databricks aitools install` installs skill files for a detected coding agent whose CLI cannot run (for example a Claude Code desktop or IDE install), and says how to switch to the plugin later. ([#7482](https://github.com/databricks/cli/pull/7482)) | |||
There was a problem hiding this comment.
[nit] The pr-checklist says a fragment that leads with the command should use the present tense with "now". Suggest: "databricks aitools install now installs skill files for a detected coding agent whose CLI cannot run …".
There was a problem hiding this comment.
Done in 7137e09: "databricks aitools install now installs skill files for a detected coding agent whose CLI is not on PATH (for example a Claude Code desktop or IDE install), unless it already has the plugin, and says how to switch to the plugin later." The PR link is kept.
3f574d9 to
46180b9
Compare
3e2f9ef to
7137e09
Compare
|
Addressed in 7137e09:
|
7137e09 to
4fd042b
Compare
|
On Question 3 (global-scope fallback next to a project or local Claude plugin), in 4fd042b: the fallback still runs, because the global skill files serve every other project and the run cannot tell which projects the user works in. When the current directory has a project or local databricks plugin for that agent, install now also warns: "Claude Code has the databricks plugin for this project, so this project will see both the plugin and the global skill files." The check reads Claude Code's own manifest for the current directory; an unreadable manifest prints no warning. Covered in |
|
On Question 1, in 4fd042b: a kept plugin now counts like an installed one. A run that sets up an agent by keeping its plugin finishes with The per-agent results keep the |
46180b9 to
544ae4b
Compare
4fd042b to
5261133
Compare
simonfaltum
left a comment
There was a problem hiding this comment.
🤖 Automated review from simonfaltum's review agent (pr-review skill) at 5261133. Each finding was verified against the code; simonfaltum may follow up.
Not approving yet: 1 open design question.
This PR makes databricks aitools install give skill files to a detected agent whose CLI isn't on PATH, and an agent that already has the plugin keeps it. It also drops stale plugin records and stops update from retrying the plugin migration for such agents. Every point from the earlier review round is addressed at this head, and no CI check has failed. Two non-blocking issues remain. Plugin agents that aren't installed at all now appear in the picker as "skills · CLI not found" and get skill files if picked. And after a run that keeps a plugin, the next aitools update writes the raw skills back next to it. There are a few nits, plus one question about picker preselection.
Questions
- In the interactive picker, a detected agent whose CLI isn't on PATH (for example Claude Code desktop) is labeled "skills · CLI not found" but stays unticked, because
IsPreselected(libs/aitools/agents/detect.go:82) only pre-checks agents that can complete an install automatically. A non-interactive run of the same command now installs skill files for that agent. Is the difference intended? Or should these agents be preselected now that they get skill files?
Agreeing with existing threads
- [nit] Telemetry per-agent results report a kept plugin as a CLI_NOT_ON_PATH failure: #7482 (comment)
| item.reason = reason | ||
| item.skipErrorCategory = protos.AitoolsErrorCategoryUnsupportedScope | ||
| } else { | ||
| case !explicit && !a.HasBinary(ctx): |
There was a problem hiding this comment.
[nice to have] The new !explicit && !a.HasBinary(ctx) branch doesn't check that the agent is detected. The interactive picker calls planItemFor for every agent in agents.Registry. So on a machine without Codex or Copilot (no config dir, no CLI), those rows now print "skills · CLI not found" where they used to print "not found". If the user picks one, skill files are written for an agent that isn't installed. That creates its config dir, so every later non-interactive run treats the agent as detected and installs for it. Before this change, that pick failed with recovery steps. Please limit the fallback to detected agents, e.g. case !explicit && !a.HasBinary(ctx) && a.Detected(ctx):, and add a not-detected case to TestPlanItemForAgentWithoutCLI.
There was a problem hiding this comment.
Fixed in 09d55e8: the branch is now case !explicit && !a.HasBinary(ctx) && a.Detected(ctx):, so an agent with neither its config dir nor its CLI is labeled "not found" in the picker again and, if picked, gets the plugin attempt and its recovery steps as before. Added a "not installed" case (Copilot) to TestPlanItemForAgentWithoutCLI.
| } | ||
| // Raw skills an earlier run gave this agent would show every skill | ||
| // twice next to the plugin, so remove them as a plugin install does. | ||
| if err := cleanupLegacyFn(ctx, it.agent, opts.Scope); err != nil { |
There was a problem hiding this comment.
[nice to have] The kept path removes the agent's raw skills but writes no plugin record to the CLI state. aitools update writes raw skills to every agent from DetectInstalled that excludePluginAgents doesn't filter, and that filter only checks state.Plugins. So the next update that changes a skill (a new release, or --force) writes the raw skills back into ~/.claude/skills next to the plugin. That brings back the duplicate listing the kept path exists to prevent. Example: Claude Code desktop plus Cursor; the user installs the plugin in Claude, runs aitools install (kept, raw skills removed), then aitools update. Please record the kept plugin in the state, or have update skip agents whose own manifest shows the plugin, and extend the acceptance sequence through an update.
There was a problem hiding this comment.
Fixed in 09d55e8 on the update side: excludePluginAgents now also drops an agent whose own manifest shows the plugin for the scope, next to the existing check of the CLI's records. I did not record the kept plugin in the state: a record would make update and uninstall run the agent's CLI for it, which is the CLI that is missing. The acceptance sequence now runs aitools update --force after the kept install and checks that Claude Code still has no skill files (it fails without the fix). Unit test: TestExcludePluginAgentsReadsTheAgentsManifest.
| project = cwd | ||
| } | ||
| } | ||
| active, err := a.DatabricksPluginActiveIn(ctx, project) |
There was a problem hiding this comment.
[nit] DatabricksPluginActiveIn counts a plugin that is disabled in Claude's settings. In the kept path, cleanupLegacyFn then removes the raw skills from an earlier fallback. A user whose plugin is installed but disabled ends up with no active databricks skills, and the run reports success. Consider removing the raw skills only when the plugin is enabled, or mention in the kept notice that the plugin is disabled.
There was a problem hiding this comment.
Leaving this one as is. The CLI has no reader for Claude Code's enabled and disabled plugins: that lives in Claude's layered settings files (user, project and local), and reading them reliably is a change of its own. A disabled plugin is also a choice the user made in Claude Code, and the kept notice already says the plugin is there and how to refresh it. Running databricks aitools install --skills-only gives the skill files back if someone wants them while the plugin stays disabled.
There was a problem hiding this comment.
Correction to my earlier reply: --skills-only is not a lasting workaround for a disabled plugin. The next default install keeps the plugin and removes those files, and update now does the same. A disabled plugin stays a known limitation of the kept path, because the CLI does not read Claude Code's enabled and disabled settings.
| # Claude Code records its plugins in installed_plugins.json. The run keeps the | ||
| # plugin and removes the skill files the previous run gave Claude Code. | ||
| "$mkdir_bin" -p "${USERPROFILE:-$HOME}/.claude/plugins" | ||
| printf '%s\n' '{"plugins":{"databricks@claude-plugins-official":[{"scope":"user","version":"0.2.9"}]}}' > "${USERPROFILE:-$HOME}/.claude/plugins/installed_plugins.json" |
There was a problem hiding this comment.
[nit] .agents/rules/testing.md says to commit static test inputs instead of creating them in script at test time. The installed_plugins.json written here with printf is a fixed literal. Please check it in next to the test and cp it into ~/.claude/plugins/.
There was a problem hiding this comment.
Done in 09d55e8: the manifest is checked in as installed_plugins.json next to the test, and the script copies it into ~/.claude/plugins/.
| // HasPluginManifest reports whether the CLI can read the agent's own plugin | ||
| // manifest (Claude Code). For such agents the manifest is authoritative: a | ||
| // plugin removed with the agent's own CLI leaves the CLI's state record behind. | ||
| func (a *Agent) HasPluginManifest() bool { |
There was a problem hiding this comment.
[nit] HasPluginManifest() returns true for Claude Code even when installed_plugins.json doesn't exist. It reports that the CLI has a manifest reader for the agent. The comment in pluginAlreadyInstalled ("When the agent has a readable plugin manifest") reads the same way. Consider renaming it to HasPluginManifestReader (or similar) so callers don't take it as a file-existence check.
There was a problem hiding this comment.
Renamed to HasPluginManifestReader in 09d55e8, with a doc comment saying it reports a reader, not that the file exists. The comment in pluginAlreadyInstalled now says "When the CLI can read the agent's own plugin manifest", and the scope rules moved into a small manifestPluginActive helper that update uses too.
| // cliMissingReason is why an agent whose CLI is not on PATH does not get the | ||
| // plugin. | ||
| func cliMissingReason(it agentPlanItem) string { | ||
| return fmt.Sprintf("'%s' is not on PATH", it.agent.Binary) |
There was a problem hiding this comment.
[nit] cliMissingReason builds '%s' is not on PATH, and pluginFailureText (recovery.go:395) builds the same string separately. Both strings appear in the acceptance output (the fallback notice and the named-agent failure). If one changes, they drift apart. Have pluginFailureText call cliMissingReason for the binary-missing case.
There was a problem hiding this comment.
Done in 09d55e8: pluginFailureText uses cliMissingReason(it) for the binary-missing case, so the fallback notice and the named-agent failure share one string.
5261133 to
09d55e8
Compare
5c39d6a to
9da45c5
Compare
8d5b0c0 to
7c43b03
Compare
9da45c5 to
0595a19
Compare
7c43b03 to
e50e58a
Compare
0595a19 to
52e34eb
Compare
e50e58a to
a5688b2
Compare
52e34eb to
181bc21
Compare
a5688b2 to
1c5b9d5
Compare
181bc21 to
e8d4f91
Compare
1c5b9d5 to
357f7ff
Compare
e8d4f91 to
2eca9b0
Compare
357f7ff to
122bf1b
Compare
2eca9b0 to
cf6eeb8
Compare
122bf1b to
461f2f4
Compare
`aitools update`, `list`, `version` and `uninstall` now log the same `aitools_command_event` funnel as `aitools install` (STARTED, then FINISHED with the outcome and error category) and resolve auth best-effort in the background so the events can upload at exit. `list` and `version` never wait for that resolution at exit, and neither do `update` and `uninstall` runs that end before their work (flag and scope errors, a declined confirm, nothing installed, a `--skills` name that is not installed), as for `install` before its plan is confirmed; their events upload only if auth has already resolved.
```diff
NewUpdateCmd / NewListCmd / NewVersionCmd / NewUninstallCmd
- return cmd
+ return instrumentCommand(cmd, protos.AitoolsCommand<X>)
+ cmd.RunE = withCommandFunnel(...) // STARTED; FINISHED from RunE's error
+ withTelemetryAuth(cmd) // background auth; no wait at exit unless
+ // RunE calls waitForTelemetryAuth (update and
+ // uninstall, once a scope has state)
```
```text
finish(err)
nil -> SUCCESS
errors.Is(err, ErrNothingInstalled) -> NOTHING_TO_DO (still exits 1 with the same text)
prompt aborted / no agents picked -> CANCELLED
otherwise -> FAILURE, classifyInstallError(err)
(scope flag errors tagged INVALID_FLAGS, as in install)
RunE returns nil, but
uninstall confirm answered "no" -> CANCELLED
uninstall removed nothing and nothing failed -> NOTHING_TO_DO
every plugin/skill update or removal attempted -> FAILURE with the first error's category
failed and nothing else succeeded (still exits 0)
(via funnelFrom(ctx).finishWith)
```
Installer additions: `ErrNothingInstalled` (same error text as before), `PluginUpdate.Err` (failed plugins are returned after the updated ones; `update` prints only the updated ones), `UpdateResult.CheckErr` (the manifest fetch error a `--check` run only warns about), and `UninstallSkillsWithResult` returning `UninstallResult{PluginsRemoved, SkillsRemoved, PluginFailures, SkillFailures}`, which replaces `UninstallSkills` and `UninstallSkillsOpts`. A plugin record for an agent this CLI does not know counts as a failed update or removal (`ErrUnknownAgent`, UNKNOWN_AGENT), and a pre-state (legacy) install returns `ErrLegacyInstall` (same text, STATE_INVALID). `uninstall --skills` with a skill that is not installed returns a `SkillError` (same text). Full uninstall now removes skills in sorted order.
Changelog: `.nextchanges/cli/aitools-telemetry-funnel.md`.
Co-authored-by: Isaac <no-reply@databricks.com>
`databricks version --check` (text output) is followed by the `apps init` skills tip on stderr when a coding agent is detected without Databricks tooling. Stdout keeps only the update-check result: ```diff Databricks CLI v0.245.0 You're on the latest version. +Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them. ``` The tip comes from `agents.MaybeVersionCheckTip`. It is not printed when an agent has the databricks plugin (in its own manifest or recorded in the CLI install state) or Databricks skills (in the shared store, in the agent's own skills dir, or in the project dirs up to the repository root), on Databricks Runtime, or when Claude Code drives the CLI (`agents.MaybeHint` already covers that caller). Changelog fragment: `.nextchanges/cli/version-check-aitools-hint.md`. Co-authored-by: Isaac <no-reply@databricks.com>
`databricks aitools install` runs `<agent> plugin --help` to check that the agent's CLI can install plugins. Agent CLIs are large Node/Rust binaries, and a cold start (first run, antivirus scan, slow container disk) can take several seconds. When the probe times out, the agent is reported as unavailable. ```diff // libs/aitools/installer/plugin.go -pluginProbeTimeout = 5 * time.Second +pluginProbeTimeout = 20 * time.Second ``` The same constant also bounds `<agent> plugin marketplace list`. Changelog fragment: `.nextchanges/cli/aitools-plugin-probe-timeout.md`. Co-authored-by: Isaac <no-reply@databricks.com>
… install on it
Claude Code's `plugin marketplace update` with no name refreshes every marketplace the user has added, so one unreachable third-party marketplace failed the Databricks plugin install. The refresh now names the marketplace the Databricks plugin is listed in, and a failed refresh no longer stops the install or the update: both go on from the local copy of the marketplace and, once that succeeds, warn and record the plugin version as unknown. Codex's `plugin marketplace upgrade` in `aitools update` names the marketplace too.
```diff
InstallPluginForAgent (libs/aitools/installer/plugin.go)
marketplace add (if absent)
- claude plugin marketplace update
- on error: remove the marketplace we added, return install-failed
+ claude plugin marketplace update <marketplace>
+ on error: keep the error, continue
claude plugin install databricks@<marketplace>
on error: remove the marketplace we added
- return install-failed with the install error
+ return install-failed with the install error
+ + "(refreshing the marketplace also failed: <refresh error>)" if the refresh failed
- record Version = <ref>
+ if the refresh failed and the marketplace was not just added by us:
+ warn that the plugin came from the local copy (with the agent's stderr)
+ record Version = "" (version unknown)
+ else record Version = <ref>
UpdatePluginForAgent (aitools update)
- claude plugin marketplace update
+ claude plugin marketplace update <recorded marketplace, or the agent's current one>
- on error: return install-failed
+ on error: keep the error, continue
claude plugin update ...
- codex plugin marketplace upgrade
+ codex plugin marketplace upgrade <recorded marketplace, or the agent's current one>
+ on error: same as above
codex plugin add ...
on update error: return install-failed
+ + "(refreshing the marketplace also failed: <refresh error>)" if the refresh failed
- record Version = <ref>
+ if the refresh failed: warn as above, and record Version = "" (version unknown)
+ unless the record already holds <ref>
+ else record Version = <ref>
```
A record without a version still counts as an installed plugin for the `databricks version --check` tip (`InstalledPluginAgents`), and `aitools update --check` shows it as "version unknown (update may be available)".
Changelog fragment: `.nextchanges/cli/aitools-marketplace-refresh.md`.
Co-authored-by: Isaac <no-reply@databricks.com>
Plugin installs no longer fail when the lookup of the latest skills release on api.github.com fails (for example HTTP 403 when rate-limited). The agent's own CLI fetches the plugin from its marketplace, so the skills ref only labels the recorded plugin version. On lookup failure the CLI now records the plugin version as unknown and keeps the scope's existing `state.Release`.
```diff
executePlan (cmd/aitools/install.go)
if plugin items
ref = GetSkillsRef(ctx)
- lookup error -> install fails
+ lookup error -> debug log, ref = "" (plugin version unknown)
install plugins
RecordPluginInstalls (libs/aitools/installer/plugin.go)
- state.Release = ref
+ if ref != "": state.Release = ref
```
`DATABRICKS_SKILLS_REF` and a pinned compat version still take precedence. Skills installs are unchanged.
Changelog fragment: `.nextchanges/cli/aitools-plugin-ref-fallback.md`.
Co-authored-by: Isaac <no-reply@databricks.com>
`aitools install` failures that used to be `UNCATEGORIZED` now get a specific `error_category`. The installer returns typed errors and `classifyInstallError` maps them:
```text
classifyInstallError(err)
BlockedError
TimedOut -> PLUGIN_CLI_TIMEOUT (agent CLI killed by its timeout)
FetchError{Stage, StatusCode, RateLimited} (release / manifest / skill file)
context.Canceled -> COMMAND_INTERRUPTED
transport failure -> NETWORK / TIMEOUT (no response, or a timeout/reset while reading the body)
RateLimited -> RATE_LIMITED (429, or 403 with GitHub's rate-limit headers)
clicompat.ErrNotFound -> MANIFEST_NOT_FOUND
Stage == release -> RELEASE_LOOKUP_FAILED
JSON syntax/type error -> MANIFEST_INVALID
otherwise -> SKILLS_FETCH_FAILED
StateError{Path} -> STATE_INVALID (unparsable .state.json)
```
- `FetchError` and `StateError` keep the wrapped error's message, so what the user sees does not change.
- A fetch that fails on the transport (no response, or a timeout or reset while reading the body) keeps reporting `NETWORK` or `TIMEOUT`, as before.
- A missing manifest wraps `clicompat.ErrNotFound`, so `clicompat.IsNotFoundError` stays the one way to check for a missing ref.
- An unreadable state file is not a `StateError`: it keeps its filesystem category.
- `FetchManifest` reads the whole body (capped at 1 MiB, like the release lookup) before parsing it, so a connection closed cleanly mid-body (`SKILLS_FETCH_FAILED`; a reset reports `NETWORK`) is told apart from an empty or truncated manifest (`MANIFEST_INVALID`).
- Changelog fragment: `.nextchanges/cli/aitools-install-fetch-error-categories.md`.
Co-authored-by: Isaac <no-reply@databricks.com>
…under its own reason
On Windows, the `codex.cmd` and `copilot.bat` shims can print the full `plugin --help` and then exit 1. Because of that, `databricks aitools install` reported a CLI that is present as `cli-not-on-path`, which points the user at the wrong fix. The probe now reports that case as `shim-exited-with-error` and still stops before running any plugin command:
```diff
probePluginCLI(agent)
bin = lookPath(agent CLI)
err = run `<bin> plugin --help` (20s timeout)
- if err: block cli-not-on-path
+ if err and windowsShimPrintedHelp(bin, err): block shim-exited-with-error
+ ("printed its plugin help but exited with an error (<exit>)[: <stderr>];
+ to see the error and exit code, run
+ `& "<bin>" plugin --help; $LASTEXITCODE` in PowerShell or
+ `"<bin>" plugin --help & echo %ERRORLEVEL%` in Command Prompt, and
+ re-run this command once it exits with 0")
+ if err: block cli-not-on-path
+windowsShimPrintedHelp(bin, err)
+ bin ends in .cmd or .bat (LookPath returns these only on Windows)
+ and err is not a timeout
+ and stdout has a `Usage: <bin> plugin` line
+ and indented `install`/`add` and `marketplace` command lines
```
`blockedErrorCategory` maps the new reason to `PLUGIN_INSTALL_FAILED`, the closest existing category.
Changelog fragment: `.nextchanges/cli/aitools-windows-shim-probe.md`.
Co-authored-by: Isaac <no-reply@databricks.com>
Coding agents often run commands in a pseudo-terminal (Cursor, Copilot in the VS Code terminal). Stdin looks like a TTY, so `databricks aitools install` showed its scope select and agent picker, and nobody was there to answer them. The run blocked. Now a caller that `useragent.AgentProvider()` identifies as an agent always gets the non-interactive defaults: global scope and every detected agent. On a terminal, when a prompt would otherwise have run, it prints a note to stderr that names the detected agent by its `--agents` value (or says more than one was detected) and the missing `--scope`/`--agents` flags of `databricks aitools install`, for example `No prompts shown because a coding agent (cursor) was detected. Pass --scope and --agents to databricks aitools install to choose what to install.` An agent env var inherited from an agent session can reach a person's shell, and the note tells them why the picker is gone. The change is scoped to install. These prompts still check only `cmdio.IsPromptSupported` and are left for a follow-up: the `Proceed?` confirm of `uninstall`, the both-scopes prompts of `update` and `uninstall`, and the `apps init` skills prompt. ```diff install RunE + interactive := cmdio.IsPromptSupported(ctx) && useragent.AgentProvider() == "" - resolveScopeWithPrompt(ctx) // prompted on any TTY + resolveScopeWithPrompt(ctx, interactive) - selectAgents(ctx, scope, skillsOnly) // picker on any TTY + selectAgents(ctx, scope, skillsOnly, interactive) - if !explicit && cmdio.IsPromptSupported(ctx) // plan summary + confirm + if !explicit && interactive ``` - The `install` help text now describes the non-interactive defaults. - The install funnel records `interactive` from the same check, so an agent caller's run is reported as non-interactive. - The interactive install tests clear the agent env with `isolateAgents`, so they keep prompting when the test binary runs under an agent (`CLAUDECODE`, `AI_AGENT`, ...). - Changelog fragment: `.nextchanges/cli/aitools-install-no-prompts-for-agents.md`. Co-authored-by: Isaac <no-reply@databricks.com>
`databricks aitools install` (interactive, no agents on the command line) no longer asks `Proceed?` after the agent picker. Agents that would be skipped are not offered in the picker, so choosing agents already confirms the plan. The install scope moves into the picker title, so it is visible before anything runs, and the plan summary after the picker shrinks to the scope line. ```diff install (interactive picker path) - promptAgentSelection # "Select agents to set up", "enter to confirm" + promptAgentSelection # "Select agents to set up (<scope> scope)", "enter to install" buildPlan - printPlanSummary # "Plan (<scope> scope):" + one line per agent - promptProceed # "Proceed?" — No => "Cancelled.", exit 0 - if !proceed: funnel CANCELLED, return nil funnel PLAN_CONFIRMED + printInstallScope # "Install scope: <scope>" install plugins / skills ``` - `promptProceed` and `defaultPromptProceed` are removed. `promptAgentSelection` takes the scope for its title. - `printPlanSummary` becomes `printInstallScope`. Its per-agent lines repeated the picker, and its skip branch could no longer run. - The picker hint reads "space to toggle, enter to install", since Enter now starts the install. - Under `--skills-only` (and the `databricks skills install` alias), every found agent is labeled "skills" in the picker, skipped rows included, because that is what they get. The removed per-agent summary lines were the only place this showed before. Rows are also pre-checked by config dir in that mode, so a plugin agent whose CLI is not on PATH starts checked, matching the non-interactive run. - `PLAN_CONFIRMED` is kept and still follows `AGENTS_SELECTED` right away; its comment in `libs/telemetry/protos` now says the picker selection is the confirmation. - `TestInstallFunnelInteractiveCancel` now cancels at the picker, which is the last prompt before the install. - Changelog fragment: `.nextchanges/cli/aitools-install-drop-proceed-confirm.md`. Co-authored-by: Isaac <no-reply@databricks.com>
`databricks aitools install` no longer stops when installing skill files fails. Agents that get a plugin (Claude Code) don't depend on the skill files, so their plugin is still installed. The skills error is still the command's error. The plugin is recorded in the install state without advancing `state.Release`, because the skill files on disk are still at the old release; otherwise `aitools version` and a pinned `aitools update` would report them as up to date. If skills from an install that predates state tracking are on disk with no state file, the plugin is not recorded at all, so `aitools update` and the install's legacy checks still see those skills. ```diff executePlan install skills for skills agents - on error: return error # plugin agents never attempted + on error: remember skillsErr install plugins for plugin agents - on state write error: return err + on state write error: return errors.Join(skillsErr, err) + record plugins; if skillsErr, keep state.Release # skills on disk are still at the old release + if skillsErr and untracked legacy skills on disk: don't create a plugin-only state file + if skillsErr: return skillsErr # stays the top-level error if per-agent errors: return agentErrors ``` Changelog fragment: `.nextchanges/cli/aitools-install-skills-failure-keeps-plugins.md`. Co-authored-by: Isaac <no-reply@databricks.com>
A failed `databricks aitools install` now ends with a step the user (or an agent calling the CLI) can take, in the text output and in a new `recovery` field of `--output json` (top level and per agent).
```text
install RunE
rerun := the run's flags (scope, agents, skills, --path, --skills-only, --experimental, --output json)
... install ...
on error: withRecovery(ctx, rerun, err) # cmd/aitools/recovery.go
per-agent errors -> no extra step (each agent printed its own)
empty picker selection -> name --scope and --agents to skip prompts
switch classifyInstallError(err) # the telemetry category
USER_ABORTED, PROMPT_FAILED -> name --scope and --agents to skip prompts
NETWORK, TIMEOUT (fetch with no response), RELEASE_LOOKUP_FAILED, RATE_LIMITED,
MANIFEST_NOT_FOUND, MANIFEST_INVALID, SKILLS_FETCH_FAILED -> per host / HTTP status / missing release
STATE_INVALID -> move the state file aside, re-run (global scope: without --skills)
SKILL_NOT_FOUND, VERSION_INCOMPATIBLE -> 'databricks aitools list', or upgrade the CLI
EXPERIMENTAL_SKILL -> no extra step (the message names --experimental)
PERMISSION_DENIED -> check ownership (e.g. an earlier sudo run)
FILESYSTEM -> check the path is writable and has free space
otherwise -> RefusedError (downgrade, legacy guard): no extra step;
else re-run with --debug, report at github.com/databricks/cli/issues
per-agent plugin failure / skip -> exact agent CLI commands
(installer.ManualPluginInstallCommands) or the run's
command narrowed to that agent
```
- The recovery step is chosen from the category `classifyInstallError` returns, so the advice and the telemetry category cannot disagree.
- Every re-run command, including the per-agent ones, repeats this run's choices (scope, `--skills-only`, `--skills`, `--experimental`, `--output json`), so a copied command never widens to every agent or the global scope. Agents chosen by flag or in the picker are named; auto-detected agents are not (the retry detects them again). A skipped agent gets the command for just that agent in the global scope.
- `--path`, `--agents` and `--skills` values are quoted for the user's shell: POSIX quoting, or the new `shellquote.WindowsArg` for cmd.exe/PowerShell. If no quoting keeps a value literal (`$`, `` ` ``, `%`, `!`, `"`, control characters on Windows; control characters on POSIX), the step says "your original install command" instead of printing a command that could expand or run something. On Windows, `--path` is printed with forward slashes, so it pastes the same into cmd.exe, PowerShell and Git Bash. A command that contains a single quote is delimited with backticks instead of single quotes.
- The installer's downgrade refusal and legacy-install guard return the new `installer.RefusedError`; their messages already name the fix, so no generic step is added when the refusal is the failure. The legacy-install message now names `--scope=global --skills-only`, the install that rebuilds skills state now that plugins are the default.
- The step appended to an error drops its final period (error strings must not end with punctuation). The error still unwraps to the original, so the telemetry category does not change.
- "No supported coding agents found" now also says how to name an agent that was not detected.
- Changelog fragment: `.nextchanges/cli/aitools-install-recovery.md`.
Co-authored-by: Isaac <no-reply@databricks.com>
`databricks aitools install` used to exit 0 when it set up nothing, so an agent calling it believed the install worked. `update` and `uninstall` did the same when every attempt failed. All three now exit with code 1 in that case.
```diff
install
- no agent detected -> message, exit 0
+ no agent detected -> message, errNoAgentSetUp (NOTHING_TO_DO)
executePlan
if an agent named via --agents failed
return its error
+ if no agent was installed
+ return errNoAgentSetUp # "nothing was installed; follow the steps above"
- no skill in the release to install -> "Installed 0 skills.", exit 0
+ no skill in the release to install -> installer.ErrNoSkillsResolved + step (NOTHING_TO_DO)
update / uninstall
- every attempt failed -> warnings, exit 0
+ every attempt failed -> "nothing was updated|checked: <cause>" / "could not remove any skill or plugin: <cause>"
```
- The FINISHED event keeps the categories it recorded before: the first failed or skipped agent's, or the first cause's for update and uninstall. A run with no agent detected, and one with no skill to install (also with `--path`), finish NOTHING_TO_DO while still exiting 1. When a plugin was installed, a release with no skill to install only skips the skills agents.
- `nothingInstalledCategory` is gone: the returned error now carries the outcome. `anyInstalled` replaces it.
- A plugin record for an agent this CLI does not know is not counted as a failed attempt: no command can act on it, so update and uninstall keep exiting 0 for it.
- Uninstall decides from `UninstallResult.SkillsChanged`, which replaces `SkillsRemoved`: the skills of which the run removed a copy an agent reads (a symlink, an entry, or the canonical dir). With `--agents`, removing only the canonical dir does not count when a targeted agent keeps its entry. "Uninstalled N skills." no longer counts skills whose removal failed.
- `update --check` no longer prints "No changes." when the manifest could not be fetched.
- The legacy `skills install` alias silences cobra's own error and usage output, so the new error prints once.
- The help text of install, update and uninstall says when they exit 1.
- Changelog fragment: `.nextchanges/notable-changes/aitools-install-nothing-installed-fails.md`.
Co-authored-by: Isaac <no-reply@databricks.com>
cf6eeb8 to
3289cf7
Compare
A detected coding agent whose CLI is not on PATH (for example a Claude Code desktop or IDE install: `~/.claude` exists, `claude` does not) used to be skipped, so the run left it with nothing. It now gets raw skill files, unless it already has the plugin. The plan decides this, so the picker label and what the run does agree, and the agent joins the one skills batch of the run: ```diff planItemFor (plugin agent, scope supported) - plugin + if CLI not on PATH, the agent is detected (config dir), and not named via --agents + if the agent already has the plugin for this scope + keep it (plugin delivery, nothing installed; outcome "kept") + else + skill files instead (skills delivery) + else + plugin ``` - "Already has the plugin": Claude Code's own `installed_plugins.json` decides (a `claude plugin uninstall` leaves the CLI's record behind); when that file exists but cannot be read, the CLI's install state for the scope decides, as it does for other agents. A global install counts only a machine-wide plugin. A project-scope install counts only a project-scope plugin of this project: its skill files are shared through the repository, while a user or local plugin is personal. For Codex and GitHub Copilot only a plugin installed by `databricks aitools` is known, so one installed another way shows every skill twice next to the skill files. - A kept plugin: install prints how to refresh it and, before the skills batch, removes raw skills an earlier run gave the agent, as a plugin install does. The picker labels it "plugin installed · CLI not found". - Skill files instead: install prints how to switch to the plugin, and after the skills install drops a stale plugin record from the CLI's install state (failure to do so is a warning). - A global-scope fallback in a directory where the agent has a project or local databricks plugin still runs, and warns that this project will see both the plugin and the global skill files. - A kept plugin counts like an installed one: the run succeeds, with telemetry outcome `NOTHING_TO_DO` when it did nothing else. - Agents named via `--agents`, and agents whose CLI is on PATH but whose `plugin` command fails, keep failing with the exact commands to run. - `aitools update` no longer tries to migrate an agent whose CLI is not on PATH from raw skills to the plugin, and no longer writes raw skills to an agent whose own manifest shows the plugin for the scope (a kept plugin has no CLI record): it removes them instead, and skips the reconcile when no agent is left that can take file skills in the scope. - New: `installer.PluginRecorded` (also used for the User-Agent tooling), `installer.ForgetPluginInstalls`, `agents.Agent.HasPluginManifestReader`, and an exported `agents.Agent.DatabricksPluginActiveIn` that reports an unreadable manifest as an error. - New acceptance test `acceptance/experimental/aitools/skills/install-non-interactive`. - Changelog fragment: `.nextchanges/cli/aitools-install-skills-fallback.md`. Co-authored-by: Isaac <no-reply@databricks.com>
461f2f4 to
eae0662
Compare
3289cf7 to
c6b1b5e
Compare
Stacked on #7481.
Changes
databricks aitools installnow gives raw skill files to a detected coding agent whose CLI is not on PATH (for example a Claude Code desktop or IDE install:~/.claudeexists,claudedoes not), unless the agent already has the plugin. Before, the run printed "Skipped Claude Code" and left the agent with nothing.The plan decides this, so the picker label and what the run does agree, and the agent joins the run's one skills batch (one ref lookup, one manifest fetch):
installed_plugins.jsondecides, becauseclaude plugin uninstallleaves the CLI's record behind. When that file exists but cannot be read, the CLI's install state for the scope decides, as it does for the other agents. A global install counts only a machine-wide plugin. A project-scope install counts only a project-scope plugin of this project: its skill files are shared through the repository, while a user or local plugin is personal.databricks aitoolsis known. If one of them got the plugin another way (for example through its app or IDE) and its CLI is not on PATH, the fallback adds skill files next to the plugin and every skill shows twice.install --helpsays so too.SUCCESS, orNOTHING_TO_DOwhen it did nothing else.--agents, and agents whose CLI is on PATH but whoseplugincommand fails, still fail with the exact commands to run.aitools update: no longer tries, and fails, to migrate an agent whose CLI is not on PATH from raw skills to the plugin; it keeps refreshing its raw skills. It also no longer writes raw skills to an agent whose own manifest shows the plugin for the scope, since install keeps such a plugin without a CLI record: it removes them instead, and skips the reconcile when no agent is left that can take file skills in the scope.installer.PluginRecorded(now also used for the User-Agent tooling),installer.ForgetPluginInstalls,agents.Agent.HasPluginManifestReader, and an exportedagents.Agent.DatabricksPluginActiveIn, which returns an error for an unreadable manifest.Why
Desktop and IDE installs of Claude Code have the config dir but no
claudeCLI, so a plaindatabricks aitools installset nothing up for them. Skill files are what such an agent can use today, and a later run with the CLI on PATH switches it to the plugin. An agent that already has the plugin keeps it, because skill files next to the plugin would list every skill twice.Tests
acceptance/experimental/aitools/skills/install-non-interactive: the detected-agent fallback in one skills batch, a kept plugin that removes the earlier skill files and anaitools update --forcethat does not write them back, the named-agent failure and its--skills-onlyrecovery, a project-scope fallback that a later user-scope plugin does not stopaitools update --scope=projectfrom refreshing, and a project-scope run that sets up nothing exiting 1.executePlanfor both cases, the funnel outcomes, the scope rules and unreadable manifest inpluginAlreadyInstalled, the project-plugin warning,ForgetPluginInstalls/PluginRecorded, the manifest reader, the project-scope plugin rule (DatabricksProjectPluginIn,excludePluginAgents), the update migration skip, and a project-scope update whose only project-capable agent has the plugin../task test-exp-aitools,./task lint-q,./task checkspass locally on Linux. Not run on Windows or macOS.Changelog fragment:
.nextchanges/cli/aitools-install-skills-fallback.md.This pull request and its description were written by Isaac.