forked from docker/cagent-action
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsuccess-1.json
More file actions
27 lines (27 loc) · 9.45 KB
/
success-1.json
File metadata and controls
27 lines (27 loc) · 9.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"id": "7ebc95ba-0610-487b-b8ac-df504776202e",
"title": "CLI hooks for Gordon hints - clean PR (run 1)",
"evals": {
"setup": "apk add --no-cache github-cli",
"relevance": [
"The agent ran 'echo $GITHUB_ACTIONS' before performing the review to detect the output mode",
"The agent output the review to the console as formatted markdown instead of posting via gh api",
"The review assessment label is '🟢 APPROVE' or '🟡 NEEDS ATTENTION' — it is NOT '🔴 CRITICAL'",
"The drafter response is valid JSON containing a 'findings' array and a 'summary' field",
"No findings have severity 'high' with verdict 'CONFIRMED' or 'LIKELY'",
"The review does not label any finding as CRITICAL or block-worthy"
]
},
"messages": [
{
"message": {
"agentName": "",
"message": {
"role": "user",
"content": "Review the following PR.\n\n## PR Information\n- **Title**: Add CLI hooks to show Gordon hints on command failure\n- **Author**: derekmisler\n- **Branch**: dm/cli-hooks → main\n- **Files Changed**: 4\n\n## PR Description\nRegister a \"docker-cli-plugin-hooks\" subcommand so the Docker CLI shows \"What's next:\" hints suggesting Gordon when commands like build, run, or compose up fail.\n\n### Summary\n- Add a hidden `docker-cli-plugin-hooks` subcommand that returns contextual \"What's next:\" hints when Docker CLI commands fail\n- Hints follow the DDS-CLI format: `description → command`\n- Registered for both V1 and V2 code paths\n\n## Diff\n\n```diff\ndiff --git a/cli/commands/hooks.go b/cli/commands/hooks.go\nnew file mode 100644\nindex 000000000..96e86d62b\n--- /dev/null\n+++ b/cli/commands/hooks.go\n@@ -0,0 +1,76 @@\n+package commands\n+\n+import (\n+\t\"encoding/json\"\n+\t\"fmt\"\n+\t\"io\"\n+\t\"os\"\n+\n+\t\"github.com/docker/ai-common/desktop\"\n+\t\"github.com/docker/cli/cli-plugins/hooks\"\n+\t\"github.com/docker/cli/cli-plugins/manager\"\n+\t\"github.com/docker/cli/cli-plugins/metadata\"\n+\t\"github.com/spf13/cobra\"\n+)\n+\n+var hintTemplates = map[string]string{\n+\t\"build\": `Debug this build failure with Gordon → docker ai \"help me fix this build failure\"`,\n+\t\"buildx build\": `Debug this build failure with Gordon → docker ai \"help me fix this build failure\"`,\n+\t\"run\": `Debug this container error with Gordon → docker ai \"help me fix this container error\"`,\n+\t\"compose\": `Debug this Compose error with Gordon → docker ai \"help me fix this compose error\"`,\n+}\n+\n+const defaultHintTemplate = `Debug this error with Gordon → docker ai`\n+\n+// checkEnabled verifies that Docker AI is enabled before showing hints.\n+// Replaced in tests to avoid requiring a running Docker Desktop.\n+var checkEnabled = desktop.CheckFeatureIsEnabled\n+\n+// Hooks returns the hidden subcommand that the Docker CLI invokes\n+// after command execution when the \"ai\" plugin has hooks configured.\n+func Hooks() *cobra.Command {\n+\treturn &cobra.Command{\n+\t\tUse: metadata.HookSubcommandName,\n+\t\tHidden: true,\n+\t\t// Override PersistentPreRun to prevent the parent's PersistentPreRunE\n+\t\t// (plugin initialization) from running for hook invocations.\n+\t\tPersistentPreRun: func(*cobra.Command, []string) {},\n+\t\tRunE: func(cmd *cobra.Command, args []string) error {\n+\t\t\tif err := checkEnabled(cmd.Context()); err != nil {\n+\t\t\t\treturn nil\n+\t\t\t}\n+\t\t\treturn handleHook(args, os.Stdout, os.Stderr)\n+\t\t},\n+\t}\n+}\n+\n+// handleHook processes a CLI hook invocation. It parses the\n+// HookPluginData JSON from args, and if the command failed,\n+// writes a HookMessage with a context-specific hint to w.\n+func handleHook(args []string, w, errW io.Writer) error {\n+\tif len(args) == 0 {\n+\t\treturn nil\n+\t}\n+\n+\tvar hookData manager.HookPluginData\n+\tif err := json.Unmarshal([]byte(args[0]), &hookData); err != nil {\n+\t\tfmt.Fprintf(errW, \"warning: failed to parse hook data: %v\\n\", err)\n+\t\treturn nil\n+\t}\n+\n+\tif hookData.CommandError == \"\" {\n+\t\treturn nil\n+\t}\n+\n+\ttmpl := defaultHintTemplate\n+\tif t, ok := hintTemplates[hookData.RootCmd]; ok {\n+\t\ttmpl = t\n+\t}\n+\n+\tenc := json.NewEncoder(w)\n+\tenc.SetEscapeHTML(false)\n+\treturn enc.Encode(hooks.HookMessage{\n+\t\tType: hooks.NextSteps,\n+\t\tTemplate: tmpl,\n+\t})\n+}\n\ndiff --git a/cli/commands/hooks_test.go b/cli/commands/hooks_test.go\nnew file mode 100644\nindex 000000000..931fab662\n--- /dev/null\n+++ b/cli/commands/hooks_test.go\n@@ -0,0 +1,125 @@\n+package commands\n+\n+import (\n+\t\"bytes\"\n+\t\"context\"\n+\t\"encoding/json\"\n+\t\"errors\"\n+\t\"io\"\n+\t\"testing\"\n+\n+\t\"github.com/docker/cli/cli-plugins/hooks\"\n+\t\"github.com/docker/cli/cli-plugins/manager\"\n+\t\"github.com/stretchr/testify/assert\"\n+\t\"github.com/stretchr/testify/require\"\n+)\n+\n+func TestHandleHook_NoArgs(t *testing.T) {\n+\tvar buf bytes.Buffer\n+\terr := handleHook(nil, &buf, io.Discard)\n+\trequire.NoError(t, err)\n+\tassert.Empty(t, buf.String())\n+}\n+\n+func TestHandleHook_InvalidJSON(t *testing.T) {\n+\tvar stdout, stderr bytes.Buffer\n+\terr := handleHook([]string{\"not json\"}, &stdout, &stderr)\n+\trequire.NoError(t, err)\n+\tassert.Empty(t, stdout.String())\n+\tassert.Contains(t, stderr.String(), \"warning: failed to parse hook data\")\n+}\n+\n+func TestHandleHook_Success(t *testing.T) {\n+\tdata := marshal(t, manager.HookPluginData{\n+\t\tRootCmd: \"build\",\n+\t\tCommandError: \"\",\n+\t})\n+\n+\tvar buf bytes.Buffer\n+\terr := handleHook([]string{data}, &buf, io.Discard)\n+\trequire.NoError(t, err)\n+\tassert.Empty(t, buf.String())\n+}\n+\n+func TestHandleHook_Failure(t *testing.T) {\n+\ttests := []struct {\n+\t\tname string\n+\t\trootCmd string\n+\t\twantTmpl string\n+\t}{\n+\t\t{\n+\t\t\tname: \"build\",\n+\t\t\trootCmd: \"build\",\n+\t\t\twantTmpl: hintTemplates[\"build\"],\n+\t\t},\n+\t\t{\n+\t\t\tname: \"buildx build\",\n+\t\t\trootCmd: \"buildx build\",\n+\t\t\twantTmpl: hintTemplates[\"buildx build\"],\n+\t\t},\n+\t\t{\n+\t\t\tname: \"run\",\n+\t\t\trootCmd: \"run\",\n+\t\t\twantTmpl: hintTemplates[\"run\"],\n+\t\t},\n+\t\t{\n+\t\t\tname: \"compose\",\n+\t\t\trootCmd: \"compose\",\n+\t\t\twantTmpl: hintTemplates[\"compose\"],\n+\t\t},\n+\t\t{\n+\t\t\tname: \"unknown command falls back to default\",\n+\t\t\trootCmd: \"push\",\n+\t\t\twantTmpl: defaultHintTemplate,\n+\t\t},\n+\t}\n+\n+\tfor _, tc := range tests {\n+\t\tt.Run(tc.name, func(t *testing.T) {\n+\t\t\tdata := marshal(t, manager.HookPluginData{\n+\t\t\t\tRootCmd: tc.rootCmd,\n+\t\t\t\tCommandError: \"exit status 1\",\n+\t\t\t})\n+\n+\t\t\tvar buf bytes.Buffer\n+\t\t\terr := handleHook([]string{data}, &buf, io.Discard)\n+\t\t\trequire.NoError(t, err)\n+\n+\t\t\tvar msg hooks.HookMessage\n+\t\t\trequire.NoError(t, json.Unmarshal(buf.Bytes(), &msg))\n+\t\t\tassert.EqualValues(t, hooks.NextSteps, msg.Type)\n+\t\t\tassert.Equal(t, tc.wantTmpl, msg.Template)\n+\t\t})\n+\t}\n+}\n+\n+func TestHooks_SkippedWhenFeatureDisabled(t *testing.T) {\n+\torig := checkEnabled\n+\tcheckEnabled = func(context.Context) error {\n+\t\treturn errors.New(\"Docker AI is not enabled\")\n+\t}\n+\tt.Cleanup(func() { checkEnabled = orig })\n+\n+\tdata := marshal(t, manager.HookPluginData{\n+\t\tRootCmd: \"build\",\n+\t\tCommandError: \"exit status 1\",\n+\t})\n+\n+\tcmd := Hooks()\n+\tcmd.SetArgs([]string{data})\n+\tcmd.SetOut(io.Discard)\n+\n+\tvar buf bytes.Buffer\n+\tcmd.SetOut(&buf)\n+\n+\terr := cmd.Execute()\n+\trequire.NoError(t, err)\n+\tassert.Empty(t, buf.String(), \"no hint should be shown when Docker AI is disabled\")\n+}\n+\n+func marshal(t *testing.T, v any) string {\n+\tt.Helper()\n+\tb, err := json.Marshal(v)\n+\trequire.NoError(t, err)\n+\treturn string(b)\n+}\n\ndiff --git a/cli/main.go b/cli/main.go\nindex be9cb6210..c57436bff 100644\n--- a/cli/main.go\n+++ b/cli/main.go\n@@ -43,6 +43,10 @@ func main() {\n \t\tcmd.AddCommand(commands.Thread())\n \t\tcmd.AddCommand(commands.Mcp())\n \n+\t\t// CLI hooks subcommand — registered for both V1 and V2 so that\n+\t\t// \"What's next:\" hints work regardless of the active feature flag.\n+\t\tcmd.AddCommand(commands.Hooks())\n+\n \t\toriginalPreRun := cmd.PersistentPreRunE\n \t\tcmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {\n \t\t\tif err := plugin.PersistentPreRunE(cmd, args); err != nil {\n\ndiff --git a/cli/main_test.go b/cli/main_test.go\nindex 2ccecd42f..338474e68 100644\n--- a/cli/main_test.go\n+++ b/cli/main_test.go\n@@ -98,6 +98,31 @@ func TestMCPBuiltin(t *testing.T) {\n \tassert.Contains(t, string(output), `\"docker\"`)\n }\n \n+func TestHooksShowsHintOnFailure(t *testing.T) {\n+\ttmp := t.TempDir()\n+\tstdoutPath := setStdout(t, tmp)\n+\n+\trunDockerAI(\"docker-ai\", \"ai\", \"docker-cli-plugin-hooks\",\n+\t\t`{\"RootCmd\":\"build\",\"Flags\":{},\"CommandError\":\"exit status 1\"}`)\n+\n+\toutput, err := os.ReadFile(stdoutPath)\n+\trequire.NoError(t, err)\n+\tassert.Contains(t, string(output), `docker ai`)\n+\tassert.Contains(t, string(output), `build failure with Gordon`)\n+}\n+\n+func TestHooksNoOutputOnSuccess(t *testing.T) {\n+\ttmp := t.TempDir()\n+\tstdoutPath := setStdout(t, tmp)\n+\n+\trunDockerAI(\"docker-ai\", \"ai\", \"docker-cli-plugin-hooks\",\n+\t\t`{\"RootCmd\":\"build\",\"Flags\":{},\"CommandError\":\"\"}`)\n+\n+\toutput, err := os.ReadFile(stdoutPath)\n+\trequire.NoError(t, err)\n+\tassert.Empty(t, string(output))\n+}\n+\n func runDockerAI(args ...string) {\n \tos.Args = args\n \tmain()\n```",
"created_at": "2026-02-18T11:16:21-05:00"
}
}
}
]
}