[object Object]

← back to Cli Printing Press

fix(cli): harden destructive auth dogfood skips (#628)

5004e4b0788dad5975eb07822417f06ca1e62957 · 2026-05-05 12:25:29 -0700 · Trevin Chow

Files touched

Diff

commit 5004e4b0788dad5975eb07822417f06ca1e62957
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Tue May 5 12:25:29 2026 -0700

    fix(cli): harden destructive auth dogfood skips (#628)
---
 internal/cli/dogfood.go                            |   2 +-
 internal/generator/generator.go                    |   5 +
 internal/generator/generator_test.go               |  36 +++++
 internal/generator/templates/auth_browser.go.tmpl  |   9 +-
 .../generator/templates/command_endpoint.go.tmpl   |   2 +-
 .../generator/templates/command_promoted.go.tmpl   |   2 +-
 internal/pipeline/live_dogfood.go                  |  80 +++++++++--
 internal/pipeline/live_dogfood_destructive_test.go |  79 +++++++++++
 internal/pipeline/live_dogfood_test.go             | 152 +++++++++++++++++++++
 internal/pipeline/mcp_size.go                      |   1 +
 internal/pipeline/mcpsync/sync.go                  |  15 +-
 internal/pipeline/mcpsync/sync_test.go             |  32 ++++-
 .../internal/cli/projects_create.go                |   2 +-
 .../internal/cli/projects_list.go                  |   2 +-
 .../internal/cli/projects_tasks_list-project.go    |   2 +-
 .../internal/cli/projects_tasks_update-project.go  |   2 +-
 .../internal/cli/promoted_public.go                |   2 +-
 .../internal/cli/items_enterprise.go               |   2 +-
 .../tier-routing-golden/internal/cli/items_list.go |   2 +-
 .../internal/cli/items_premium.go                  |   2 +-
 20 files changed, 397 insertions(+), 34 deletions(-)

diff --git a/internal/cli/dogfood.go b/internal/cli/dogfood.go
index 72a9ede3..6154c0bf 100644
--- a/internal/cli/dogfood.go
+++ b/internal/cli/dogfood.go
@@ -90,7 +90,7 @@ func newDogfoodCmd() *cobra.Command {
 	cmd.Flags().DurationVar(&timeout, "timeout", 30*time.Second, "Timeout for each live dogfood test")
 	cmd.Flags().StringVar(&writeAcceptance, "write-acceptance", "", "Write phase5-acceptance.json to this path when live dogfood passes")
 	cmd.Flags().StringVar(&authEnv, "auth-env", "", "Environment variable that proves an API credential was available for the acceptance marker")
-	cmd.Flags().BoolVar(&allowDestructive, "allow-destructive", false, "Re-enable testing of endpoints classified as destructive-at-auth (path/annotation matches refresh/rotate/revoke). Default skips them to prevent runner-credential rotation.")
+	cmd.Flags().BoolVar(&allowDestructive, "allow-destructive", false, "Re-enable testing of endpoints classified as destructive-at-auth. Default skips them to prevent runner-credential rotation.")
 	_ = cmd.MarkFlagRequired("dir")
 	return cmd
 }
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 58fb758e..8bf275b9 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -617,6 +617,7 @@ type clientTemplateData struct {
 type endpointTemplateData struct {
 	ResourceName    string
 	ResourceBaseURL string
+	EffectivePath   string
 	EffectiveTier   string
 	FuncPrefix      string
 	CommandPath     string
@@ -1546,6 +1547,7 @@ func (g *Generator) renderResourceCommands(promotedResourceNames map[string]bool
 			epData := endpointTemplateData{
 				ResourceName:    name,
 				ResourceBaseURL: strings.TrimRight(resource.BaseURL, "/"),
+				EffectivePath:   effectiveEndpointPath(resource, endpoint),
 				EffectiveTier:   g.Spec.EffectiveTier(resource, endpoint),
 				FuncPrefix:      name,
 				CommandPath:     name,
@@ -1612,6 +1614,7 @@ func (g *Generator) renderResourceCommands(promotedResourceNames map[string]bool
 				epData := endpointTemplateData{
 					ResourceName:    subName,
 					ResourceBaseURL: subResourceBaseURL,
+					EffectivePath:   effectiveSubEndpointPath(resource, subResource, endpoint),
 					EffectiveTier:   g.Spec.EffectiveTier(effectiveResource, endpoint),
 					FuncPrefix:      name + "-" + subName,
 					CommandPath:     name + " " + subName,
@@ -2014,6 +2017,7 @@ func (g *Generator) renderPromotedCommandFiles(promotedCommands []PromotedComman
 			PromotedName  string
 			ResourceName  string
 			EndpointName  string
+			EffectivePath string
 			Endpoint      spec.Endpoint
 			EffectiveTier string
 			HasStore      bool
@@ -2025,6 +2029,7 @@ func (g *Generator) renderPromotedCommandFiles(promotedCommands []PromotedComman
 			PromotedName:  pc.PromotedName,
 			ResourceName:  pc.ResourceName,
 			EndpointName:  pc.EndpointName,
+			EffectivePath: effectiveEndpointPath(resource, pc.Endpoint),
 			Endpoint:      pc.Endpoint,
 			EffectiveTier: g.Spec.EffectiveTier(resource, pc.Endpoint),
 			HasStore:      g.VisionSet.Store,
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index ac27bbfa..eaaf2c0c 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -491,6 +491,42 @@ func TestGenerateAgentContextCommand(t *testing.T) {
 	assert.Contains(t, payload, "cli")
 	assert.Contains(t, payload, "auth")
 	assert.Contains(t, payload, "commands")
+
+	annotatedEndpoint := findAgentContextCommand(payload["commands"], func(command map[string]any) bool {
+		annotations, ok := command["annotations"].(map[string]any)
+		if !ok {
+			return false
+		}
+		return annotations["pp:endpoint"] != "" && annotations["mcp:read-only"] == "true"
+	})
+	require.NotNil(t, annotatedEndpoint, "agent-context must surface endpoint and read-only annotations")
+
+	unannotatedParent := findAgentContextCommand(payload["commands"], func(command map[string]any) bool {
+		_, hasAnnotations := command["annotations"]
+		subcommands, hasSubcommands := command["subcommands"].([]any)
+		return !hasAnnotations && hasSubcommands && len(subcommands) > 0
+	})
+	require.NotNil(t, unannotatedParent, "agent-context must keep annotations omitted when absent")
+}
+
+func findAgentContextCommand(root any, match func(map[string]any) bool) map[string]any {
+	commands, ok := root.([]any)
+	if !ok {
+		return nil
+	}
+	for _, raw := range commands {
+		command, ok := raw.(map[string]any)
+		if !ok {
+			continue
+		}
+		if match(command) {
+			return command
+		}
+		if found := findAgentContextCommand(command["subcommands"], match); found != nil {
+			return found
+		}
+	}
+	return nil
 }
 
 func TestGenerateOAuth2AuthTemplateConditionally(t *testing.T) {
diff --git a/internal/generator/templates/auth_browser.go.tmpl b/internal/generator/templates/auth_browser.go.tmpl
index ec03c239..cfc9de52 100644
--- a/internal/generator/templates/auth_browser.go.tmpl
+++ b/internal/generator/templates/auth_browser.go.tmpl
@@ -608,10 +608,11 @@ func newAuthRefreshQueriesCmd(flags *rootFlags) *cobra.Command {
 	var duration time.Duration
 	var outputPath string
 	cmd := &cobra.Command{
-		Use:     "refresh-queries",
-		Short:   "Refresh captured GraphQL persisted-query hashes",
-		Long:    "Refresh captured GraphQL persisted-query hashes for CLIs generated from browser traffic with the graphql_persisted_query hint.",
-		Example: "  {{.Name}}-pp-cli auth refresh-queries --duration 45s",
+		Use:         "refresh-queries",
+		Short:       "Refresh captured GraphQL persisted-query hashes",
+		Long:        "Refresh captured GraphQL persisted-query hashes for CLIs generated from browser traffic with the graphql_persisted_query hint.",
+		Example:     "  {{.Name}}-pp-cli auth refresh-queries --duration 45s",
+		Annotations: map[string]string{"pp:destructive-auth": "false"},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cfg, err := config.Load(flags.configPath)
 			if err != nil {
diff --git a/internal/generator/templates/command_endpoint.go.tmpl b/internal/generator/templates/command_endpoint.go.tmpl
index 4c0e0d7d..92c62328 100644
--- a/internal/generator/templates/command_endpoint.go.tmpl
+++ b/internal/generator/templates/command_endpoint.go.tmpl
@@ -52,7 +52,7 @@ func new{{camel .FuncPrefix}}{{camel .EndpointName}}Cmd(flags *rootFlags) *cobra
 {{- end}}
 		Short: "{{oneline .Endpoint.Description}}",
 		Example: "{{exampleLine .CommandPath .EndpointName .Endpoint}}",
-		Annotations: map[string]string{"pp:endpoint": "{{.ResourceName}}.{{.EndpointName}}"{{if .IsReadOnly}}, "mcp:read-only": "true"{{end}}},
+		Annotations: map[string]string{"pp:endpoint": {{printf "%q" (printf "%s.%s" .ResourceName .EndpointName)}}, "pp:method": {{printf "%q" (upper .Endpoint.Method)}}, "pp:path": {{printf "%q" .EffectivePath}}{{if .IsReadOnly}}, "mcp:read-only": "true"{{end}}},
 		RunE: func(cmd *cobra.Command, args []string) error {
 {{- if positionalArgs .Endpoint}}
 			if len(args) == 0 {
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index b6a521f5..ef768ddb 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -29,7 +29,7 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 		Short: "{{oneline .Endpoint.Description}}",
 		Long:  "Shortcut for '{{.ResourceName}} {{.EndpointName}}'. {{oneline .Endpoint.Description}}",
 		Example: "  {{modulePath}} {{.PromotedName}}",
-		Annotations: map[string]string{"pp:endpoint": "{{.ResourceName}}.{{.EndpointName}}"{{if .IsReadOnly}}, "mcp:read-only": "true"{{end}}},
+		Annotations: map[string]string{"pp:endpoint": {{printf "%q" (printf "%s.%s" .ResourceName .EndpointName)}}, "pp:method": {{printf "%q" (upper .Endpoint.Method)}}, "pp:path": {{printf "%q" .EffectivePath}}{{if .IsReadOnly}}, "mcp:read-only": "true"{{end}}},
 		RunE: func(cmd *cobra.Command, args []string) error {
 {{- range .Endpoint.Params}}
 {{- if and .Required (not .Positional) (not .Default)}}
diff --git a/internal/pipeline/live_dogfood.go b/internal/pipeline/live_dogfood.go
index bea61c10..028cf24c 100644
--- a/internal/pipeline/live_dogfood.go
+++ b/internal/pipeline/live_dogfood.go
@@ -33,8 +33,8 @@ const (
 	LiveDogfoodTestErrorReal LiveDogfoodTestKind = "error_path_real"
 )
 
-// reasonDestructiveAtAuth is the Skip reason emitted for endpoints whose
-// path or pp:endpoint annotation matches refresh/rotate/revoke. Reused
+// reasonDestructiveAtAuth is the Skip reason emitted for endpoints that
+// can invalidate the credential used by the live-dogfood runner. Reused
 // across the matrix builder, the flag help text, and the test fixtures.
 const reasonDestructiveAtAuth = "destructive-at-auth"
 
@@ -873,28 +873,76 @@ func skippedLiveDogfoodResult(command string, kind LiveDogfoodTestKind, reason s
 	}
 }
 
-// destructiveAuthTerms are case-insensitive substrings classifying a
-// command as destructive-at-auth.
-var destructiveAuthTerms = []string{"refresh", "rotate", "revoke"}
+const (
+	endpointAnnotation        = "pp:endpoint"
+	endpointMethodAnnotation  = "pp:method"
+	endpointPathAnnotation    = "pp:path"
+	mcpReadOnlyAnnotation     = "mcp:read-only"
+	destructiveAuthAnnotation = "pp:destructive-auth"
+)
+
+// destructiveAuthTerms are case-insensitive command or endpoint tokens
+// classifying a command as destructive-at-auth.
+var destructiveAuthTerms = map[string]bool{
+	"refresh":    true,
+	"rotate":     true,
+	"revoke":     true,
+	"regenerate": true,
+	"reset":      true,
+	"cycle":      true,
+}
+
+var destructiveAuthResources = map[string]bool{
+	"api-keys": true,
+	"api_keys": true,
+	"sessions": true,
+	"tokens":   true,
+}
 
-// isDestructiveAtAuth reports whether a command rotates or revokes the
-// bearer the live-dogfood runner is using. Reads pp:endpoint
+// isDestructiveAtAuth reports whether a command can invalidate the bearer
+// the live-dogfood runner is using. Reads pp:endpoint
 // (authoritative for endpoint-mirror commands) and falls back to
 // path-segment matching across the command path for novel commands.
 // Read-only commands are exempt regardless of name.
 func isDestructiveAtAuth(annotations map[string]string, commandPath []string) bool {
-	if annotations["mcp:read-only"] == "true" {
+	if v, ok := annotations[destructiveAuthAnnotation]; ok {
+		return annotationIsTrueValue(v)
+	}
+	if annotationIsTrueValue(annotations[mcpReadOnlyAnnotation]) {
 		return false
 	}
-	if endpoint := annotations["pp:endpoint"]; endpoint != "" {
-		return containsAnyOf(strings.ToLower(endpoint), destructiveAuthTerms)
+	if endpoint := annotations[endpointAnnotation]; endpoint != "" {
+		if containsDestructiveAuthTerm(endpoint) {
+			return true
+		}
+		if strings.EqualFold(strings.TrimSpace(annotations[endpointMethodAnnotation]), "DELETE") &&
+			endpointTargetsAuthResource(endpoint, annotations[endpointPathAnnotation]) {
+			return true
+		}
+		return false
 	}
-	for _, seg := range commandPath {
-		if containsAnyOf(strings.ToLower(seg), destructiveAuthTerms) {
+	return slices.ContainsFunc(commandPath, containsDestructiveAuthTerm)
+}
+
+func containsDestructiveAuthTerm(s string) bool {
+	tokens := strings.FieldsFunc(strings.ToLower(s), func(r rune) bool {
+		return r < 'a' || r > 'z'
+	})
+	return slices.ContainsFunc(tokens, func(token string) bool {
+		return destructiveAuthTerms[token]
+	})
+}
+
+func endpointTargetsAuthResource(endpoint, path string) bool {
+	for _, segment := range splitPath(path) {
+		segment = strings.ToLower(strings.Trim(segment, "{}:"))
+		if destructiveAuthResources[segment] {
 			return true
 		}
 	}
-	return false
+	return slices.ContainsFunc(strings.Split(strings.ToLower(endpoint), "."), func(segment string) bool {
+		return destructiveAuthResources[segment]
+	})
 }
 
 func liveDogfoodHappyArgs(command liveDogfoodCommand) ([]string, bool) {
@@ -972,13 +1020,17 @@ func finalizeLiveDogfoodReport(report *LiveDogfoodReport) {
 		}
 	}
 	// Failed-or-empty wins: any real failure or a fully empty matrix is FAIL.
+	// Quick runs whose whole slice was explicitly skipped are no-signal, not
+	// a failing matrix: the classifier did its job and avoided live mutation.
 	// The quick-level PASS arm uses min(5, MatrixSize) for the threshold so
 	// single-command quick runs (~4 entries when all probes succeed) can
 	// PASS, while keeping a MatrixSize >= 4 floor that blocks pathological
 	// matrices where most entries skipped.
 	switch {
-	case report.Failed > 0 || report.MatrixSize == 0:
+	case report.Failed > 0 || (report.MatrixSize == 0 && (report.Level != "quick" || report.Skipped == 0)):
 		report.Verdict = "FAIL"
+	case report.Level == "quick" && report.MatrixSize == 0 && report.Skipped > 0:
+		report.Verdict = "PASS"
 	case report.Level == "quick" && report.MatrixSize >= 4 && report.Passed+report.Skipped >= min(5, report.MatrixSize):
 		report.Verdict = "PASS"
 	case report.Level == "quick":
diff --git a/internal/pipeline/live_dogfood_destructive_test.go b/internal/pipeline/live_dogfood_destructive_test.go
index 45a6b855..cbeaa745 100644
--- a/internal/pipeline/live_dogfood_destructive_test.go
+++ b/internal/pipeline/live_dogfood_destructive_test.go
@@ -32,6 +32,73 @@ func TestIsDestructiveAtAuth(t *testing.T) {
 			path:        []string{"my-cli", "tokens"},
 			want:        true,
 		},
+		{
+			name:        "annotation primary regenerate",
+			annotations: map[string]string{"pp:endpoint": "api-keys.regenerate-key"},
+			path:        []string{"my-cli", "api-keys"},
+			want:        true,
+		},
+		{
+			name:        "annotation primary reset",
+			annotations: map[string]string{"pp:endpoint": "passwords.reset-token"},
+			path:        []string{"my-cli", "passwords"},
+			want:        true,
+		},
+		{
+			name:        "annotation primary cycle",
+			annotations: map[string]string{"pp:endpoint": "tokens.cycle"},
+			path:        []string{"my-cli", "tokens"},
+			want:        true,
+		},
+		{
+			name: "delete api-keys endpoint by method and path",
+			annotations: map[string]string{
+				"pp:endpoint": "api-keys.delete-key",
+				"pp:method":   "DELETE",
+				"pp:path":     "/v1/api-keys/{id}",
+			},
+			path: []string{"my-cli", "api-keys"},
+			want: true,
+		},
+		{
+			name: "delete sessions endpoint by method and path",
+			annotations: map[string]string{
+				"pp:endpoint": "sessions.destroy",
+				"pp:method":   "DELETE",
+				"pp:path":     "/sessions/{session_id}",
+			},
+			path: []string{"my-cli", "sessions"},
+			want: true,
+		},
+		{
+			name: "delete tokens endpoint by method and endpoint resource fallback",
+			annotations: map[string]string{
+				"pp:endpoint": "tokens.delete",
+				"pp:method":   "DELETE",
+			},
+			path: []string{"my-cli", "tokens"},
+			want: true,
+		},
+		{
+			name: "delete ordinary resource is not destructive-at-auth",
+			annotations: map[string]string{
+				"pp:endpoint": "users.delete-user",
+				"pp:method":   "DELETE",
+				"pp:path":     "/users/{id}",
+			},
+			path: []string{"my-cli", "users"},
+			want: false,
+		},
+		{
+			name: "delete auth resource without DELETE method is not method-classified",
+			annotations: map[string]string{
+				"pp:endpoint": "api-keys.delete-preview",
+				"pp:method":   "GET",
+				"pp:path":     "/api-keys/{id}",
+			},
+			path: []string{"my-cli", "api-keys"},
+			want: false,
+		},
 		{
 			name:        "annotation case-insensitive",
 			annotations: map[string]string{"pp:endpoint": "API-Keys.Refresh-Key"},
@@ -74,6 +141,18 @@ func TestIsDestructiveAtAuth(t *testing.T) {
 			path:        []string{"my-cli", "store", "refresh"},
 			want:        false,
 		},
+		{
+			name:        "destructive-auth false annotation exempts generated refresh helper",
+			annotations: map[string]string{"pp:destructive-auth": "false"},
+			path:        []string{"my-cli", "auth", "refresh-queries"},
+			want:        false,
+		},
+		{
+			name:        "destructive-auth true annotation opts in",
+			annotations: map[string]string{"pp:destructive-auth": "true"},
+			path:        []string{"my-cli", "auth", "metadata"},
+			want:        true,
+		},
 		{
 			name: "empty inputs",
 			want: false,
diff --git a/internal/pipeline/live_dogfood_test.go b/internal/pipeline/live_dogfood_test.go
index f59727de..419dff3c 100644
--- a/internal/pipeline/live_dogfood_test.go
+++ b/internal/pipeline/live_dogfood_test.go
@@ -108,6 +108,58 @@ func TestRunLiveDogfoodErrorPathAcceptsExpectedNonZeroExit(t *testing.T) {
 	assert.Equal(t, 2, errorPath.ExitCode)
 }
 
+func TestRunLiveDogfoodSkipsDestructiveByDefault(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("test uses a shell script as the fake binary; skip on Windows")
+	}
+
+	dir, binaryName := writeLiveDogfoodDestructiveFixture(t)
+	report, err := RunLiveDogfood(LiveDogfoodOptions{
+		CLIDir:     dir,
+		BinaryName: binaryName,
+		Level:      "full",
+		Timeout:    2 * time.Second,
+	})
+	require.NoError(t, err)
+
+	var destructiveSkips int
+	for _, result := range report.Tests {
+		if result.Command == "api-keys refresh" {
+			assert.Equal(t, LiveDogfoodStatusSkip, result.Status)
+			assert.Equal(t, reasonDestructiveAtAuth, result.Reason)
+			destructiveSkips++
+		}
+	}
+	assert.Equal(t, 4, destructiveSkips)
+
+	widgetsHelp := findResultByCommandKind(report, "widgets list", LiveDogfoodTestHelp)
+	require.NotNil(t, widgetsHelp)
+	assert.Equal(t, LiveDogfoodStatusPass, widgetsHelp.Status)
+}
+
+func TestRunLiveDogfoodAllowDestructiveBypass(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("test uses a shell script as the fake binary; skip on Windows")
+	}
+
+	dir, binaryName := writeLiveDogfoodDestructiveFixture(t)
+	report, err := RunLiveDogfood(LiveDogfoodOptions{
+		CLIDir:           dir,
+		BinaryName:       binaryName,
+		Level:            "full",
+		Timeout:          2 * time.Second,
+		AllowDestructive: true,
+	})
+	require.NoError(t, err)
+
+	assert.Equal(t, 0, countResultsWithReason(report.Tests, reasonDestructiveAtAuth))
+	for _, kind := range []LiveDogfoodTestKind{LiveDogfoodTestHelp, LiveDogfoodTestHappy, LiveDogfoodTestJSON} {
+		result := findResultByCommandKind(report, "api-keys refresh", kind)
+		require.NotNil(t, result)
+		assert.Equal(t, LiveDogfoodStatusPass, result.Status, result.Reason)
+	}
+}
+
 func TestRunLiveDogfoodExplicitBinaryNameMustExist(t *testing.T) {
 	dir := t.TempDir()
 
@@ -216,6 +268,12 @@ func TestFinalizeLiveDogfoodReportVerdictGate(t *testing.T) {
 			name:    "quick all skip — MatrixSize 0",
 			level:   "quick",
 			results: []LiveDogfoodTestResult{mkResult(LiveDogfoodStatusSkip), mkResult(LiveDogfoodStatusSkip)},
+			want:    "PASS",
+		},
+		{
+			name:    "full all skip — MatrixSize 0 still blocks acceptance",
+			level:   "full",
+			results: []LiveDogfoodTestResult{mkResult(LiveDogfoodStatusSkip), mkResult(LiveDogfoodStatusSkip)},
 			want:    "FAIL",
 		},
 		{
@@ -788,6 +846,90 @@ exit 99
 	return dir, binaryName
 }
 
+func writeLiveDogfoodDestructiveFixture(t *testing.T) (dir string, binaryName string) {
+	t.Helper()
+
+	dir = t.TempDir()
+	binaryName = "fixture-pp-cli"
+	writeTestManifestForLiveDogfood(t, dir)
+
+	binPath := filepath.Join(dir, binaryName)
+	script := `#!/bin/sh
+set -u
+
+if [ "$1" = "agent-context" ]; then
+  cat <<'JSON'
+{
+  "commands": [
+    {"name":"api-keys","subcommands":[
+      {"name":"refresh","annotations":{"pp:endpoint":"api-keys.keys-refresh"}}
+    ]},
+    {"name":"widgets","subcommands":[
+      {"name":"list"}
+    ]}
+  ]
+}
+JSON
+  exit 0
+fi
+
+if [ "$1" = "api-keys" ] && [ "$2" = "refresh" ] && [ "${3:-}" = "--help" ]; then
+  cat <<'HELP'
+Refresh the current API key.
+
+Usage:
+  fixture-pp-cli api-keys refresh [flags]
+
+Examples:
+  fixture-pp-cli api-keys refresh
+
+Flags:
+      --json    Output JSON
+HELP
+  exit 0
+fi
+
+if [ "$1" = "api-keys" ] && [ "$2" = "refresh" ]; then
+  if [ "${3:-}" = "--json" ]; then
+    echo '{"refreshed":true}'
+    exit 0
+  fi
+  echo 'refreshed'
+  exit 0
+fi
+
+if [ "$1" = "widgets" ] && [ "$2" = "list" ] && [ "${3:-}" = "--help" ]; then
+  cat <<'HELP'
+List widgets.
+
+Usage:
+  fixture-pp-cli widgets list [flags]
+
+Examples:
+  fixture-pp-cli widgets list
+
+Flags:
+      --json    Output JSON
+HELP
+  exit 0
+fi
+
+if [ "$1" = "widgets" ] && [ "$2" = "list" ]; then
+  if [ "${3:-}" = "--json" ]; then
+    echo '{"results":[]}'
+    exit 0
+  fi
+  echo 'widgets'
+  exit 0
+fi
+
+echo "unexpected args: $*" >&2
+exit 99
+`
+	require.NoError(t, os.WriteFile(binPath, []byte(script), 0o755))
+	return dir, binaryName
+}
+
 func writeTestManifestForLiveDogfood(t *testing.T, dir string) {
 	t.Helper()
 	require.NoError(t, WriteCLIManifest(dir, CLIManifest{
@@ -1373,6 +1515,16 @@ func findResultByCommandKind(report *LiveDogfoodReport, command string, kind Liv
 	return nil
 }
 
+func countResultsWithReason(results []LiveDogfoodTestResult, reason string) int {
+	count := 0
+	for _, result := range results {
+		if result.Reason == reason {
+			count++
+		}
+	}
+	return count
+}
+
 // setupRichFixture is the shared preamble for U2/U3 tests: skip on Windows,
 // build the rich fixture, and enable the argv-log side channel via a unique
 // per-test tempfile path. Returns the fixture dir, binary name, and the
diff --git a/internal/pipeline/mcp_size.go b/internal/pipeline/mcp_size.go
index 5498b17e..44b3b30e 100644
--- a/internal/pipeline/mcp_size.go
+++ b/internal/pipeline/mcp_size.go
@@ -419,6 +419,7 @@ func cobratreeCommandKind(cmd cobraCommandLiteral) mcpCobraCommandKind {
 }
 
 func annotationIsTrueValue(v string) bool {
+	v = strings.ToLower(strings.TrimSpace(v))
 	return v == "true" || v == "1" || v == "yes"
 }
 
diff --git a/internal/pipeline/mcpsync/sync.go b/internal/pipeline/mcpsync/sync.go
index 9ddd5b8d..33752e74 100644
--- a/internal/pipeline/mcpsync/sync.go
+++ b/internal/pipeline/mcpsync/sync.go
@@ -32,7 +32,8 @@ var (
 	errAnnotationSoftFail = errors.New("endpoint annotation skipped")
 )
 
-var endpointAnnotationLine = regexp.MustCompile(`(?m)^\s*Annotations: map\[string\]string\{"pp:endpoint": "[^"]+"(?:, "mcp:read-only": "true")?\},\s*$`)
+var endpointAnnotationLine = regexp.MustCompile(`(?m)^\s*Annotations: map\[string\]string\{"pp:endpoint": "[^"]+", "pp:method": "[^"]+", "pp:path": "[^"]+"(?:, "mcp:read-only": "true")?\},\s*$`)
+var staleEndpointAnnotationLine = regexp.MustCompile(`(?m)^\s*Annotations: map\[string\]string\{"pp:endpoint": "[^"]+"(?:, "mcp:read-only": "true")?\},\s*$`)
 
 type Result struct {
 	Changed bool
@@ -300,6 +301,14 @@ func ensureEndpointAnnotation(path, annotationLine string) error {
 		return fmt.Errorf("reading endpoint command %s: %w", path, err)
 	}
 	src := string(data)
+	if endpointAnnotationLine.MatchString(src) {
+		return nil
+	}
+	annotationLine = strings.TrimSuffix(annotationLine, "\n")
+	if staleEndpointAnnotationLine.MatchString(src) {
+		next := staleEndpointAnnotationLine.ReplaceAllString(src, annotationLine)
+		return writeFileAtomic(path, []byte(next))
+	}
 	if strings.Contains(src, `"pp:endpoint"`) {
 		return nil
 	}
@@ -320,9 +329,7 @@ func ensureEndpointAnnotation(path, annotationLine string) error {
 		return fmt.Errorf("%s does not match the generated endpoint command shape; cannot add endpoint MCP annotation", path)
 	}
 
-	if !strings.HasSuffix(annotationLine, "\n") {
-		annotationLine += "\n"
-	}
+	annotationLine += "\n"
 	next := src[:insertAt] + annotationLine + src[insertAt:]
 	return writeFileAtomic(path, []byte(next))
 }
diff --git a/internal/pipeline/mcpsync/sync_test.go b/internal/pipeline/mcpsync/sync_test.go
index b066d084..ac458241 100644
--- a/internal/pipeline/mcpsync/sync_test.go
+++ b/internal/pipeline/mcpsync/sync_test.go
@@ -71,13 +71,43 @@ func RegisterNovelFeatureTools() { shellOutToCLI("projects list") }
 
 	updatedEndpoint, err := os.ReadFile(endpointPath)
 	require.NoError(t, err)
-	assert.Contains(t, string(updatedEndpoint), `Annotations: map[string]string{"pp:endpoint": "projects.list", "mcp:read-only": "true"}`)
+	assert.Contains(t, string(updatedEndpoint), `Annotations: map[string]string{"pp:endpoint": "projects.list", "pp:method": "GET", "pp:path": "/projects", "mcp:read-only": "true"}`)
 
 	updatedTools, err := os.ReadFile(filepath.Join(cliDir, "internal", "mcp", "tools.go"))
 	require.NoError(t, err)
 	assert.Contains(t, string(updatedTools), "cobratree.RegisterAll")
 }
 
+func TestEnsureEndpointAnnotationUpgradesStaleEndpointAnnotation(t *testing.T) {
+	t.Parallel()
+
+	path := filepath.Join(t.TempDir(), "api_keys_delete.go")
+	oldAnnotation := `Annotations: map[string]string{"pp:endpoint": "api-keys.delete"}`
+	src := `// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
+package cli
+
+func newAPIKeysDeleteCmd() {
+	cmd := struct {
+		Use         string
+		Annotations map[string]string
+	}{
+		Use: "delete",
+		` + oldAnnotation + `,
+	}
+	_ = cmd
+}
+`
+	require.NoError(t, os.WriteFile(path, []byte(src), 0o644))
+
+	newAnnotation := `		Annotations: map[string]string{"pp:endpoint": "api-keys.delete", "pp:method": "DELETE", "pp:path": "/api-keys/{id}"},`
+	require.NoError(t, ensureEndpointAnnotation(path, newAnnotation))
+
+	updated, err := os.ReadFile(path)
+	require.NoError(t, err)
+	assert.Contains(t, string(updated), newAnnotation)
+	assert.NotContains(t, string(updated), oldAnnotation)
+}
+
 func TestEnsureRootCmdExportPreservesRootFlagPointer(t *testing.T) {
 	t.Parallel()
 
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
index da95c738..578330a7 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_create.go
@@ -22,7 +22,7 @@ func newProjectsCreateCmd(flags *rootFlags) *cobra.Command {
 		Use:   "create",
 		Short: "Create project",
 		Example: "  printing-press-golden-pp-cli projects create --name example-resource",
-		Annotations: map[string]string{"pp:endpoint": "projects.create"},
+		Annotations: map[string]string{"pp:endpoint": "projects.create", "pp:method": "POST", "pp:path": "/projects"},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if !stdinBody {
 				if !cmd.Flags().Changed("name") && !flags.dryRun {
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
index c25b028a..1384bd9f 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_list.go
@@ -21,7 +21,7 @@ func newProjectsListCmd(flags *rootFlags) *cobra.Command {
 		Use:   "list",
 		Short: "List projects",
 		Example: "  printing-press-golden-pp-cli projects list",
-		Annotations: map[string]string{"pp:endpoint": "projects.list", "mcp:read-only": "true"},
+		Annotations: map[string]string{"pp:endpoint": "projects.list", "pp:method": "GET", "pp:path": "/projects", "mcp:read-only": "true"},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if cmd.Flags().Changed("status") {
 				allowedStatus := []string{ "draft", "active", "archived" }
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
index 57dc171c..ff35ec4f 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_list-project.go
@@ -22,7 +22,7 @@ func newProjectsTasksListProjectCmd(flags *rootFlags) *cobra.Command {
 		Aliases: []string{"get"},
 		Short: "List project tasks",
 		Example: "  printing-press-golden-pp-cli projects tasks list-project 550e8400-e29b-41d4-a716-446655440000",
-		Annotations: map[string]string{"pp:endpoint": "tasks.list-project", "mcp:read-only": "true"},
+		Annotations: map[string]string{"pp:endpoint": "tasks.list-project", "pp:method": "GET", "pp:path": "/projects/{projectId}/tasks", "mcp:read-only": "true"},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if len(args) == 0 {
 				return cmd.Help()
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
index 313acb9d..0587b7d1 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/projects_tasks_update-project.go
@@ -23,7 +23,7 @@ func newProjectsTasksUpdateProjectCmd(flags *rootFlags) *cobra.Command {
 		Aliases: []string{"update"},
 		Short: "Update project task",
 		Example: "  printing-press-golden-pp-cli projects tasks update-project 550e8400-e29b-41d4-a716-446655440000 550e8400-e29b-41d4-a716-446655440000",
-		Annotations: map[string]string{"pp:endpoint": "tasks.update-project"},
+		Annotations: map[string]string{"pp:endpoint": "tasks.update-project", "pp:method": "PATCH", "pp:path": "/projects/{projectId}/tasks/{taskId}"},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if len(args) == 0 {
 				return cmd.Help()
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
index 212bb3b1..57fce0ef 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/promoted_public.go
@@ -18,7 +18,7 @@ func newPublicPromotedCmd(flags *rootFlags) *cobra.Command {
 		Short: "Get public service status",
 		Long:  "Shortcut for 'public get-status'. Get public service status",
 		Example: "  printing-press-golden-pp-cli public",
-		Annotations: map[string]string{"pp:endpoint": "public.get-status", "mcp:read-only": "true"},
+		Annotations: map[string]string{"pp:endpoint": "public.get-status", "pp:method": "GET", "pp:path": "/public/status", "mcp:read-only": "true"},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			c, err := flags.newClient()
 			if err != nil {
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go
index cf1cc5a6..c8bd6924 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_enterprise.go
@@ -17,7 +17,7 @@ func newItemsEnterpriseCmd(flags *rootFlags) *cobra.Command {
 		Use:   "enterprise",
 		Short: "List enterprise items",
 		Example: "  tier-routing-golden-pp-cli items enterprise",
-		Annotations: map[string]string{"pp:endpoint": "items.enterprise", "mcp:read-only": "true"},
+		Annotations: map[string]string{"pp:endpoint": "items.enterprise", "pp:method": "GET", "pp:path": "/items/enterprise", "mcp:read-only": "true"},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			c, err := flags.newClient()
 			if err != nil {
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go
index a62bd071..e8376e6e 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_list.go
@@ -18,7 +18,7 @@ func newItemsListCmd(flags *rootFlags) *cobra.Command {
 		Use:   "list",
 		Short: "List free items",
 		Example: "  tier-routing-golden-pp-cli items list",
-		Annotations: map[string]string{"pp:endpoint": "items.list", "mcp:read-only": "true"},
+		Annotations: map[string]string{"pp:endpoint": "items.list", "pp:method": "GET", "pp:path": "/items", "mcp:read-only": "true"},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			c, err := flags.newClient()
 			if err != nil {
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go
index 80e7d7fb..1a594ba2 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/items_premium.go
@@ -17,7 +17,7 @@ func newItemsPremiumCmd(flags *rootFlags) *cobra.Command {
 		Use:   "premium",
 		Short: "List paid items",
 		Example: "  tier-routing-golden-pp-cli items premium",
-		Annotations: map[string]string{"pp:endpoint": "items.premium", "mcp:read-only": "true"},
+		Annotations: map[string]string{"pp:endpoint": "items.premium", "pp:method": "GET", "pp:path": "/items/premium", "mcp:read-only": "true"},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			c, err := flags.newClient()
 			if err != nil {

← 622e15f5 docs(cli): capture mcp handler path-vs-query positional recu  ·  back to Cli Printing Press  ·  fix(cli): correct no-auth 403 hints (#629) 013f92ad →