[object Object]

← back to Cli Printing Press

feat(cli): improve generated money workflows and artifact safety (#653)

6d9ab664b06e1a1c24c0014af14cae4bd40cb0f1 · 2026-05-07 01:53:35 -0500 · Cathryn Lavery

Files touched

Diff

commit 6d9ab664b06e1a1c24c0014af14cae4bd40cb0f1
Author: Cathryn Lavery <50469282+cathrynlavery@users.noreply.github.com>
Date:   Thu May 7 01:53:35 2026 -0500

    feat(cli): improve generated money workflows and artifact safety (#653)
---
 catalog/mercury.yaml                               |  27 ++
 internal/artifacts/secrets.go                      |  53 +++
 internal/artifacts/secrets_test.go                 |  43 +++
 internal/catalog/catalog.go                        |   6 +
 internal/catalog/catalog_test.go                   |  20 ++
 internal/cli/generate_test.go                      |  19 ++
 internal/cli/root.go                               |  29 +-
 internal/generator/generator.go                    | 279 ++++++++++++++--
 internal/generator/generator_test.go               | 359 ++++++++++++++++++++-
 internal/generator/templates/auth_simple.go.tmpl   |   2 +-
 .../generator/templates/channel_workflow.go.tmpl   | 215 ++++++++++++
 .../generator/templates/command_promoted.go.tmpl   |   6 -
 internal/openapi/parser.go                         | 145 +++++++--
 internal/openapi/parser_test.go                    | 163 ++++++++++
 internal/pipeline/fullrun.go                       |   2 +
 internal/pipeline/live_dogfood.go                  | 107 ++++--
 internal/pipeline/live_dogfood_test.go             |  96 ++++--
 testdata/golden/expected/catalog-list/stdout.txt   |   1 +
 .../printing-press-rich-auth/internal/cli/auth.go  |   2 +-
 .../cafe-bistro/internal/types/types.go            |   1 -
 .../printing-press-golden/internal/cli/auth.go     |   2 +-
 .../printing-press-golden/internal/types/types.go  |   1 -
 22 files changed, 1464 insertions(+), 114 deletions(-)

diff --git a/catalog/mercury.yaml b/catalog/mercury.yaml
new file mode 100644
index 00000000..d16bbcbd
--- /dev/null
+++ b/catalog/mercury.yaml
@@ -0,0 +1,27 @@
+name: mercury
+display_name: Mercury
+description: Business banking API for accounts, transactions, payments, recipients, cards, invoices, treasury, and webhooks
+category: payments
+spec_url: https://dash.readme.com/api/v1/api-registry/ilz81omom1n4vx
+spec_format: json
+openapi_version: "3.0"
+tier: official
+verified_date: "2026-05-06"
+homepage: https://docs.mercury.com/reference
+owner_name: Cathryn Lavery
+mcp:
+  transport: [stdio, http]
+  orchestration: code
+  endpoint_tools: hidden
+notes: |
+  Official Mercury API spec hosted through ReadMe's public API registry. Mercury
+  also ships an official Go CLI; the Printing Press value is the generated MCP
+  surface, local store/search/SQL workflows, and agent safety annotations.
+
+  Treat money-moving operations as high-risk during dogfood. The current parser
+  preserves complex request body fields so generated dry-run and agent-plan
+  commands can show the exact payload before any write call is sent.
+known_alternatives:
+  - name: mercury-cli
+    url: https://github.com/MercuryTechnologies/mercury-cli
+    language: go
diff --git a/internal/artifacts/secrets.go b/internal/artifacts/secrets.go
new file mode 100644
index 00000000..857e45f1
--- /dev/null
+++ b/internal/artifacts/secrets.go
@@ -0,0 +1,53 @@
+package artifacts
+
+import "regexp"
+
+type secretReplacement struct {
+	pattern     *regexp.Regexp
+	replacement string
+}
+
+var archivedSpecSecretPatterns = []secretReplacement{
+	{
+		pattern:     regexp.MustCompile(`secret-token:[A-Za-z0-9][A-Za-z0-9:_-]{20,}`),
+		replacement: `secret-token:<REDACTED_TOKEN_EXAMPLE>`,
+	},
+	{
+		pattern:     regexp.MustCompile(`Bearer sk_(?:live|test)_[A-Za-z0-9]{8,}`),
+		replacement: `Bearer <REDACTED_STRIPE_TOKEN_EXAMPLE>`,
+	},
+	{
+		pattern:     regexp.MustCompile(`Bearer (?:ghp|gho)_[A-Za-z0-9]{20,}`),
+		replacement: `Bearer <REDACTED_GITHUB_TOKEN_EXAMPLE>`,
+	},
+	{
+		pattern:     regexp.MustCompile(`Bearer github_pat_[A-Za-z0-9_]{40,}`),
+		replacement: `Bearer <REDACTED_GITHUB_TOKEN_EXAMPLE>`,
+	},
+	{
+		pattern:     regexp.MustCompile(`(?i)(access[_-]?token|api[_-]?key|secret)=([A-Za-z0-9._+/=-]{20,})`),
+		replacement: `${1}=<REDACTED_CREDENTIAL_EXAMPLE>`,
+	},
+	{
+		pattern:     regexp.MustCompile(`(?i)(Authorization:\s*Bearer\s+)[A-Za-z0-9._~+/=-]{20,}`),
+		replacement: `${1}<REDACTED_BEARER_TOKEN_EXAMPLE>`,
+	},
+	{
+		pattern:     regexp.MustCompile(`(?i)((?:X-API-Key|API-Key):\s*)[A-Za-z0-9._+/=-]{20,}`),
+		replacement: `${1}<REDACTED_CREDENTIAL_EXAMPLE>`,
+	},
+	{
+		pattern:     regexp.MustCompile(`(?i)((?:"?(?:access[_-]?token|api[_-]?key|apikey|secret|client[_-]?secret)"?\s*:\s*)"?)([A-Za-z0-9._+/=-]{20,})("?)`),
+		replacement: `${1}<REDACTED_CREDENTIAL_EXAMPLE>${3}`,
+	},
+}
+
+// RedactArchivedSpecSecrets removes credential-shaped examples from archived
+// specs while keeping surrounding auth documentation intact.
+func RedactArchivedSpecSecrets(data []byte) []byte {
+	out := append([]byte(nil), data...)
+	for _, rule := range archivedSpecSecretPatterns {
+		out = rule.pattern.ReplaceAll(out, []byte(rule.replacement))
+	}
+	return out
+}
diff --git a/internal/artifacts/secrets_test.go b/internal/artifacts/secrets_test.go
new file mode 100644
index 00000000..f5142513
--- /dev/null
+++ b/internal/artifacts/secrets_test.go
@@ -0,0 +1,43 @@
+package artifacts
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestRedactArchivedSpecSecretsRemovesVendorTokenExamples(t *testing.T) {
+	input := []byte(`Examples:
+Authorization: Bearer secret-token:vendor_production_wma_24SCp4G81X3yHL4Wq8FgzuaP9ye3VKf2mgTDctXyRg5HY_example
+Authorization: Bearer sk_live_1234567890abcdefghijkl
+Authorization: Bearer ghp_1234567890abcdefghijklmnopqrstuvwx
+Authorization: Bearer abcdefghijklmnopqrstuvwxyz123456
+X-API-Key: abcdefghijklmnopqrstuvwxyz123456
+"apiKey": "abcdefghijklmnopqrstuvwxyz123456"
+api_key: abcdefghijklmnopqrstuvwxyz123456
+https://api.example.com/widgets?access_token=abcdefghijklmnopqrstuvwxyz123456
+`)
+
+	got := string(RedactArchivedSpecSecrets(input))
+
+	require.Contains(t, got, "Authorization: Bearer secret-token:<REDACTED_TOKEN_EXAMPLE>")
+	require.Contains(t, got, "Authorization: Bearer <REDACTED_STRIPE_TOKEN_EXAMPLE>")
+	require.Contains(t, got, "Authorization: Bearer <REDACTED_GITHUB_TOKEN_EXAMPLE>")
+	require.Contains(t, got, "Authorization: Bearer <REDACTED_BEARER_TOKEN_EXAMPLE>")
+	require.Contains(t, got, "X-API-Key: <REDACTED_CREDENTIAL_EXAMPLE>")
+	require.Contains(t, got, `"apiKey": "<REDACTED_CREDENTIAL_EXAMPLE>"`)
+	require.Contains(t, got, "api_key: <REDACTED_CREDENTIAL_EXAMPLE>")
+	require.Contains(t, got, "access_token=<REDACTED_CREDENTIAL_EXAMPLE>")
+	require.NotContains(t, got, "vendor_production")
+	require.NotContains(t, got, "sk_live_1234567890")
+	require.NotContains(t, got, "ghp_1234567890")
+	require.NotContains(t, got, "abcdefghijklmnopqrstuvwxyz123456")
+}
+
+func TestRedactArchivedSpecSecretsKeepsPlaceholders(t *testing.T) {
+	input := []byte("Use Authorization: Bearer TOKEN or MERCURY_BEARER_AUTH=your-token-here")
+
+	got := string(RedactArchivedSpecSecrets(input))
+
+	require.Equal(t, string(input), got)
+}
diff --git a/internal/catalog/catalog.go b/internal/catalog/catalog.go
index b71f6a5d..a6b98625 100644
--- a/internal/catalog/catalog.go
+++ b/internal/catalog/catalog.go
@@ -9,6 +9,7 @@ import (
 	"sort"
 	"strings"
 
+	"github.com/mvanhorn/cli-printing-press/v3/internal/spec"
 	"gopkg.in/yaml.v3"
 )
 
@@ -117,6 +118,8 @@ type Entry struct {
 	VerifiedDate      string     `yaml:"verified_date"`
 	Homepage          string     `yaml:"homepage"`
 	Notes             string     `yaml:"notes"`
+	Owner             string     `yaml:"owner,omitempty"`
+	OwnerName         string     `yaml:"owner_name,omitempty"`
 	KnownAlternatives []KnownAlt `yaml:"known_alternatives,omitempty"`
 	SandboxEndpoint   string     `yaml:"sandbox_endpoint,omitempty"`
 	// SpecSource describes how the spec was obtained. Empty defaults to "official".
@@ -130,6 +133,9 @@ type Entry struct {
 	// HTTPTransport describes the runtime HTTP transport. Empty defaults by provenance:
 	// official uses standard; non-official web-discovered sources use browser-chrome.
 	HTTPTransport string `yaml:"http_transport,omitempty"`
+	// MCP describes generation-time MCP surface choices for catalog specs whose
+	// upstream OpenAPI documents do not carry Printing Press extensions.
+	MCP spec.MCPConfig `yaml:"mcp,omitempty"`
 	// ProxyRoutes maps path prefixes to backend service names for proxy-envelope APIs.
 	// Only relevant when ClientPattern is "proxy-envelope".
 	ProxyRoutes map[string]string `yaml:"proxy_routes,omitempty"`
diff --git a/internal/catalog/catalog_test.go b/internal/catalog/catalog_test.go
index 6c0af93a..5168879a 100644
--- a/internal/catalog/catalog_test.go
+++ b/internal/catalog/catalog_test.go
@@ -21,6 +21,12 @@ openapi_version: "3.0"
 tier: community
 verified_date: "2026-03-23"
 homepage: https://example.com
+owner: test-owner
+owner_name: Trevin Chow
+mcp:
+  transport: [stdio, http]
+  orchestration: code
+  endpoint_tools: hidden
 notes: Example fixture.
 `)
 
@@ -37,6 +43,11 @@ notes: Example fixture.
 	assert.Equal(t, "community", entry.Tier)
 	assert.Equal(t, "2026-03-23", entry.VerifiedDate)
 	assert.Equal(t, "https://example.com", entry.Homepage)
+	assert.Equal(t, "test-owner", entry.Owner)
+	assert.Equal(t, "Trevin Chow", entry.OwnerName)
+	assert.Equal(t, []string{"stdio", "http"}, entry.MCP.Transport)
+	assert.Equal(t, "code", entry.MCP.Orchestration)
+	assert.Equal(t, "hidden", entry.MCP.EndpointTools)
 	assert.Equal(t, "Example fixture.", entry.Notes)
 }
 
@@ -455,6 +466,15 @@ func TestLookupFSFindsProductHunt(t *testing.T) {
 	assert.False(t, *entry.AuthRequired)
 }
 
+func TestLookupFSFindsMercuryCatalogGenerationMetadata(t *testing.T) {
+	entry, err := LookupFS(catalogfs.FS, "mercury")
+	require.NoError(t, err)
+	assert.NotEmpty(t, entry.OwnerName)
+	assert.Equal(t, []string{"stdio", "http"}, entry.MCP.Transport)
+	assert.Equal(t, "code", entry.MCP.Orchestration)
+	assert.Equal(t, "hidden", entry.MCP.EndpointTools)
+}
+
 func TestLookupFSNotFound(t *testing.T) {
 	_, err := LookupFS(catalogfs.FS, "nonexistent-api")
 	require.Error(t, err)
diff --git a/internal/cli/generate_test.go b/internal/cli/generate_test.go
index 76aa212e..a45d1ee9 100644
--- a/internal/cli/generate_test.go
+++ b/internal/cli/generate_test.go
@@ -8,6 +8,7 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/mvanhorn/cli-printing-press/v3/internal/catalog"
 	"github.com/mvanhorn/cli-printing-press/v3/internal/pipeline"
 	"github.com/mvanhorn/cli-printing-press/v3/internal/spec"
 	"github.com/stretchr/testify/assert"
@@ -765,3 +766,21 @@ resources:
 	assert.NoDirExists(t, derivedDir,
 		"the spec-derived directory must not be created when --output is explicit")
 }
+
+func TestEnrichSpecFromCatalogCopiesGenerationMetadata(t *testing.T) {
+	apiSpec := &spec.APISpec{Name: "test-api"}
+
+	enrichSpecFromCatalogEntry(apiSpec, &catalog.Entry{
+		OwnerName: "Trevin Chow",
+		MCP: spec.MCPConfig{
+			Transport:     []string{"stdio", "http"},
+			Orchestration: "code",
+			EndpointTools: "hidden",
+		},
+	})
+
+	assert.Equal(t, "Trevin Chow", apiSpec.OwnerName)
+	assert.Equal(t, []string{"stdio", "http"}, apiSpec.MCP.Transport)
+	assert.Equal(t, "code", apiSpec.MCP.Orchestration)
+	assert.Equal(t, "hidden", apiSpec.MCP.EndpointTools)
+}
diff --git a/internal/cli/root.go b/internal/cli/root.go
index fbbeb8a5..ce4f6b8a 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -15,6 +15,7 @@ import (
 	"time"
 
 	catalogfs "github.com/mvanhorn/cli-printing-press/v3/catalog"
+	"github.com/mvanhorn/cli-printing-press/v3/internal/artifacts"
 	"github.com/mvanhorn/cli-printing-press/v3/internal/browsersniff"
 	"github.com/mvanhorn/cli-printing-press/v3/internal/catalog"
 	"github.com/mvanhorn/cli-printing-press/v3/internal/docspec"
@@ -349,7 +350,8 @@ func newGenerateCmd() *cobra.Command {
 				if json.Valid(specRawBytes[0]) {
 					archiveName = "spec.json"
 				}
-				if err := os.WriteFile(filepath.Join(absOut, archiveName), specRawBytes[0], 0o644); err != nil {
+				data := artifacts.RedactArchivedSpecSecrets(specRawBytes[0])
+				if err := os.WriteFile(filepath.Join(absOut, archiveName), data, 0o644); err != nil {
 					fmt.Fprintf(os.Stderr, "warning: could not archive spec: %v\n", err)
 				}
 			}
@@ -1101,6 +1103,13 @@ func enrichSpecFromCatalog(apiSpec *spec.APISpec) {
 	if err != nil {
 		return
 	}
+	enrichSpecFromCatalogEntry(apiSpec, entry)
+}
+
+func enrichSpecFromCatalogEntry(apiSpec *spec.APISpec, entry *catalog.Entry) {
+	if apiSpec == nil || entry == nil {
+		return
+	}
 	if len(entry.ProxyRoutes) > 0 && len(apiSpec.ProxyRoutes) == 0 {
 		apiSpec.ProxyRoutes = entry.ProxyRoutes
 	}
@@ -1110,9 +1119,18 @@ func enrichSpecFromCatalog(apiSpec *spec.APISpec) {
 	if entry.Category != "" && apiSpec.Category == "" {
 		apiSpec.Category = entry.Category
 	}
+	if entry.Owner != "" && apiSpec.Owner == "" {
+		apiSpec.Owner = entry.Owner
+	}
+	if entry.OwnerName != "" && apiSpec.OwnerName == "" {
+		apiSpec.OwnerName = entry.OwnerName
+	}
 	if entry.HTTPTransport != "" && apiSpec.HTTPTransport == "" {
 		apiSpec.HTTPTransport = entry.HTTPTransport
 	}
+	if mcpConfigured(entry.MCP) && !mcpConfigured(apiSpec.MCP) {
+		apiSpec.MCP = entry.MCP
+	}
 	if entry.BearerRefresh.BundleURL != "" && apiSpec.BearerRefresh.BundleURL == "" {
 		apiSpec.BearerRefresh.BundleURL = entry.BearerRefresh.BundleURL
 	}
@@ -1120,3 +1138,12 @@ func enrichSpecFromCatalog(apiSpec *spec.APISpec) {
 		apiSpec.BearerRefresh.Pattern = entry.BearerRefresh.Pattern
 	}
 }
+
+func mcpConfigured(m spec.MCPConfig) bool {
+	return len(m.Transport) > 0 ||
+		m.Addr != "" ||
+		len(m.Intents) > 0 ||
+		m.EndpointTools != "" ||
+		m.Orchestration != "" ||
+		m.OrchestrationThreshold != 0
+}
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 8ccb0932..6797e4ae 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -1647,30 +1647,24 @@ func (g *Generator) renderResourceCommands(promotedResourceNames map[string]bool
 
 		// Sub-resource parent + endpoint files
 		for subName, subResource := range resource.SubResources {
-			// Skip single-endpoint sub-resource parents under promoted resources.
-			// The promoted command wires the endpoint directly, making the parent dead code.
-			// Multi-endpoint sub-resource parents are still needed (the promoted command uses them).
-			skipSubParent := promotedResourceNames[name] && len(subResource.Endpoints) == 1
-			if !skipSubParent {
-				subParentData := struct {
-					ResourceName string
-					FuncPrefix   string
-					CommandPath  string
-					Resource     spec.Resource
-					Hidden       bool
-					*spec.APISpec
-				}{
-					ResourceName: subName,
-					FuncPrefix:   name + "-" + subName,
-					CommandPath:  name + " " + subName,
-					Resource:     subResource,
-					Hidden:       false,
-					APISpec:      g.Spec,
-				}
-				subParentPath := filepath.Join("internal", "cli", safeResourceFileStem(name+"_"+subName)+".go")
-				if err := g.renderTemplate("command_parent.go.tmpl", subParentPath, subParentData); err != nil {
-					return fmt.Errorf("rendering sub-parent %s/%s: %w", name, subName, err)
-				}
+			subParentData := struct {
+				ResourceName string
+				FuncPrefix   string
+				CommandPath  string
+				Resource     spec.Resource
+				Hidden       bool
+				*spec.APISpec
+			}{
+				ResourceName: subName,
+				FuncPrefix:   name + "-" + subName,
+				CommandPath:  name + " " + subName,
+				Resource:     subResource,
+				Hidden:       false,
+				APISpec:      g.Spec,
+			}
+			subParentPath := filepath.Join("internal", "cli", safeResourceFileStem(name+"_"+subName)+".go")
+			if err := g.renderTemplate("command_parent.go.tmpl", subParentPath, subParentData); err != nil {
+				return fmt.Errorf("rendering sub-parent %s/%s: %w", name, subName, err)
 			}
 
 			// Sub-resources inherit the parent's BaseURL override; an
@@ -1910,6 +1904,7 @@ type visionRenderData struct {
 	SearchEndpointMethod   string
 	SearchBodyFields       []profiler.SearchBodyField
 	GraphQLFieldPaths      map[string]string
+	AgentMoneyWorkflow     AgentMoneyWorkflow
 }
 
 func (g *Generator) visionRenderData(schema []TableDef) visionRenderData {
@@ -1932,6 +1927,7 @@ func (g *Generator) visionRenderData(schema []TableDef) visionRenderData {
 		SearchEndpointMethod:   g.profile.SearchEndpointMethod,
 		SearchBodyFields:       g.profile.SearchBodyFields,
 		GraphQLFieldPaths:      gqlFieldPaths,
+		AgentMoneyWorkflow:     detectAgentMoneyWorkflow(g.Spec, g.PromotedEndpointNames),
 	}
 }
 
@@ -1984,12 +1980,14 @@ func (g *Generator) renderWorkflowFiles(visionData visionRenderData) ([]string,
 	if g.VisionSet.Store {
 		workflowData := struct {
 			*spec.APISpec
-			SyncableResources []profiler.SyncableResource
-			SearchableFields  map[string][]string
+			SyncableResources  []profiler.SyncableResource
+			SearchableFields   map[string][]string
+			AgentMoneyWorkflow AgentMoneyWorkflow
 		}{
-			APISpec:           g.Spec,
-			SyncableResources: g.profile.SyncableResources,
-			SearchableFields:  g.profile.SearchableFields,
+			APISpec:            g.Spec,
+			SyncableResources:  g.profile.SyncableResources,
+			SearchableFields:   g.profile.SearchableFields,
+			AgentMoneyWorkflow: visionData.AgentMoneyWorkflow,
 		}
 		if err := g.renderTemplate("channel_workflow.go.tmpl", filepath.Join("internal", "cli", "channel_workflow.go"), workflowData); err != nil {
 			return nil, fmt.Errorf("rendering workflow: %w", err)
@@ -2013,6 +2011,223 @@ func (g *Generator) renderWorkflowFiles(visionData visionRenderData) ([]string,
 	return renderedWorkflowConstructors, nil
 }
 
+type AgentMoneyWorkflow struct {
+	Payment  *AgentMoneyCommand
+	Request  *AgentMoneyCommand
+	Transfer *AgentMoneyCommand
+}
+
+func (w AgentMoneyWorkflow) Enabled() bool {
+	return w.Payment != nil || w.Request != nil || w.Transfer != nil
+}
+
+func (w AgentMoneyWorkflow) complete() bool {
+	return w.Payment != nil && w.Request != nil && w.Transfer != nil
+}
+
+type AgentMoneyCommand struct {
+	CommandPath              []string
+	HasAccountIDPosition     bool
+	AmountFlag               string
+	AmountInteger            bool
+	RecipientIDFlag          string
+	PaymentMethodFlag        string
+	IdempotencyKeyFlag       string
+	SourceAccountIDFlag      string
+	DestinationAccountIDFlag string
+	NoteFlag                 string
+	ExternalMemoFlag         string
+	PurposeFlag              string
+}
+
+type agentMoneyKind string
+
+const (
+	agentMoneyKindNone     agentMoneyKind = ""
+	agentMoneyKindPayment  agentMoneyKind = "payment"
+	agentMoneyKindRequest  agentMoneyKind = "request"
+	agentMoneyKindTransfer agentMoneyKind = "transfer"
+)
+
+func detectAgentMoneyWorkflow(api *spec.APISpec, promotedEndpointNames map[string]string) AgentMoneyWorkflow {
+	var workflow AgentMoneyWorkflow
+	if api == nil {
+		return workflow
+	}
+
+	for _, resourceName := range sortedResourceNames(api.Resources) {
+		resource := api.Resources[resourceName]
+		for _, endpointName := range sortedEndpointNames(resource.Endpoints) {
+			endpoint := resource.Endpoints[endpointName]
+			class, cmd := agentMoneyCommandForEndpoint(endpoint, []string{toKebab(resourceName), toKebab(endpointName)}, promotedEndpointNames[resourceName] == endpointName)
+			assignAgentMoneyCommand(&workflow, class, cmd)
+			if workflow.complete() {
+				return workflow
+			}
+		}
+		for _, subName := range sortedResourceNames(resource.SubResources) {
+			sub := resource.SubResources[subName]
+			for _, endpointName := range sortedEndpointNames(sub.Endpoints) {
+				endpoint := sub.Endpoints[endpointName]
+				class, cmd := agentMoneyCommandForEndpoint(endpoint, []string{toKebab(resourceName), toKebab(subName), toKebab(endpointName)}, false)
+				assignAgentMoneyCommand(&workflow, class, cmd)
+				if workflow.complete() {
+					return workflow
+				}
+			}
+		}
+	}
+
+	return workflow
+}
+
+func assignAgentMoneyCommand(workflow *AgentMoneyWorkflow, class agentMoneyKind, cmd *AgentMoneyCommand) {
+	if workflow == nil || cmd == nil {
+		return
+	}
+	switch class {
+	case agentMoneyKindTransfer:
+		if workflow.Transfer == nil {
+			workflow.Transfer = cmd
+		}
+	case agentMoneyKindRequest:
+		if workflow.Request == nil {
+			workflow.Request = cmd
+		}
+	case agentMoneyKindPayment:
+		if workflow.Payment == nil {
+			workflow.Payment = cmd
+		}
+	}
+}
+
+func classifyAgentMoneyEndpoint(endpoint spec.Endpoint, body map[string]spec.Param) agentMoneyKind {
+	if !strings.EqualFold(endpoint.Method, "POST") {
+		return agentMoneyKindNone
+	}
+	has := func(name string) bool {
+		_, ok := body[strings.ToLower(name)]
+		return ok
+	}
+	if has("amount") && has("sourceAccountId") && has("destinationAccountId") && has("idempotencyKey") {
+		return agentMoneyKindTransfer
+	}
+	if has("amount") && has("recipientId") && has("paymentMethod") && has("idempotencyKey") {
+		if strings.Contains(strings.ToLower(endpoint.Path), "request") {
+			return agentMoneyKindRequest
+		}
+		return agentMoneyKindPayment
+	}
+	return agentMoneyKindNone
+}
+
+func agentMoneyCommandForEndpoint(endpoint spec.Endpoint, path []string, promoted bool) (agentMoneyKind, *AgentMoneyCommand) {
+	body := paramsByLowerName(endpoint.Body)
+	class := classifyAgentMoneyEndpoint(endpoint, body)
+	if class == agentMoneyKindNone {
+		return class, nil
+	}
+	if !agentMoneyEndpointHasSupportedPositionals(endpoint) || !agentMoneyEndpointHasSupportedRequiredBody(endpoint, class) {
+		return class, nil
+	}
+	if promoted && len(path) >= 1 {
+		path = path[:1]
+	} else if endpoint.Alias != "" && len(path) > 0 {
+		path[len(path)-1] = endpoint.Alias
+	}
+	cmd := &AgentMoneyCommand{CommandPath: path}
+	for _, param := range endpoint.Params {
+		if param.Positional && strings.EqualFold(param.Name, "accountId") {
+			cmd.HasAccountIDPosition = true
+			break
+		}
+	}
+	if p, ok := body["amount"]; ok {
+		cmd.AmountFlag = flagName(paramIdent(p))
+		cmd.AmountInteger = primitiveKind(p.Type) == "int"
+	}
+	if p, ok := body["recipientid"]; ok {
+		cmd.RecipientIDFlag = flagName(paramIdent(p))
+	}
+	if p, ok := body["paymentmethod"]; ok {
+		cmd.PaymentMethodFlag = flagName(paramIdent(p))
+	}
+	if p, ok := body["idempotencykey"]; ok {
+		cmd.IdempotencyKeyFlag = flagName(paramIdent(p))
+	}
+	if p, ok := body["sourceaccountid"]; ok {
+		cmd.SourceAccountIDFlag = flagName(paramIdent(p))
+	}
+	if p, ok := body["destinationaccountid"]; ok {
+		cmd.DestinationAccountIDFlag = flagName(paramIdent(p))
+	}
+	if p, ok := body["note"]; ok {
+		cmd.NoteFlag = flagName(paramIdent(p))
+	}
+	if p, ok := body["externalmemo"]; ok {
+		cmd.ExternalMemoFlag = flagName(paramIdent(p))
+	}
+	if p, ok := body["purpose"]; ok {
+		cmd.PurposeFlag = flagName(paramIdent(p))
+	}
+	return class, cmd
+}
+
+func agentMoneyEndpointHasSupportedPositionals(endpoint spec.Endpoint) bool {
+	for _, param := range endpoint.Params {
+		if !param.Positional || !param.Required {
+			continue
+		}
+		if !strings.EqualFold(param.Name, "accountId") {
+			return false
+		}
+	}
+	return true
+}
+
+func agentMoneyEndpointHasSupportedRequiredBody(endpoint spec.Endpoint, class agentMoneyKind) bool {
+	required := map[string]struct{}{}
+	switch class {
+	case agentMoneyKindTransfer:
+		for _, name := range []string{"amount", "sourceaccountid", "destinationaccountid", "idempotencykey"} {
+			required[name] = struct{}{}
+		}
+	case agentMoneyKindRequest, agentMoneyKindPayment:
+		for _, name := range []string{"amount", "recipientid", "paymentmethod", "idempotencykey"} {
+			required[name] = struct{}{}
+		}
+	default:
+		return false
+	}
+
+	for _, param := range endpoint.Body {
+		if !param.Required {
+			continue
+		}
+		if _, ok := required[strings.ToLower(param.Name)]; !ok {
+			return false
+		}
+	}
+	return true
+}
+
+func paramsByLowerName(params []spec.Param) map[string]spec.Param {
+	out := make(map[string]spec.Param, len(params))
+	for _, param := range params {
+		out[strings.ToLower(param.Name)] = param
+	}
+	return out
+}
+
+func sortedResourceNames(resources map[string]spec.Resource) []string {
+	names := make([]string, 0, len(resources))
+	for name := range resources {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+	return names
+}
+
 func (g *Generator) renderInsightFiles() []string {
 	var renderedInsightConstructors []string
 
@@ -2284,7 +2499,9 @@ func (g *Generator) renderTemplate(tmplName, outPath string, data any) error {
 		return err
 	}
 
-	return os.WriteFile(fullPath, buf.Bytes(), 0o644)
+	rendered := bytes.TrimRight(buf.Bytes(), " \t\r\n")
+	rendered = append(rendered, '\n')
+	return os.WriteFile(fullPath, rendered, 0o644)
 }
 
 func validateRenderedArtifact(outPath, content string) error {
@@ -3129,7 +3346,7 @@ func (g *Generator) exampleLine(commandPath, endpointName string, endpoint spec.
 	var parts []string
 	parts = append(parts, naming.CLI(g.Spec.Name))
 	parts = append(parts, strings.Fields(commandPath)...)
-	parts = append(parts, endpointName)
+	parts = append(parts, toKebab(endpointName))
 	parts = append(parts, commandExampleArgParts(endpoint)...)
 
 	return "  " + strings.Join(parts, " ")
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 4cff2fc0..e4ce9254 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -3391,6 +3391,363 @@ func TestGeneratedOutput_PromotedCommandExists(t *testing.T) {
 	assert.NoFileExists(t, filepath.Join(outputDir, "internal", "cli", "users.go"))
 }
 
+func TestGeneratedOutput_PromotedCommandKeepsSubresourceParents(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := &spec.APISpec{
+		Name:    "promsub",
+		Version: "0.1.0",
+		BaseURL: "https://api.example.com",
+		Auth:    spec.AuthConfig{Type: "api_key", Header: "X-Api-Key", EnvVars: []string{"PROM_SUB_API_KEY"}},
+		Config:  spec.ConfigSpec{Format: "toml", Path: "~/.config/promsub-pp-cli/config.toml"},
+		Resources: map[string]spec.Resource{
+			"account": {
+				Description: "Manage accounts",
+				Endpoints: map[string]spec.Endpoint{
+					"get": {
+						Method:      "GET",
+						Path:        "/account/{accountId}",
+						Description: "Get account",
+						Params:      []spec.Param{{Name: "accountId", Type: "string", Required: true, Positional: true}},
+					},
+				},
+				SubResources: map[string]spec.Resource{
+					"cards": {
+						Description: "Manage cards",
+						Endpoints: map[string]spec.Endpoint{
+							"get-account": {
+								Method:      "GET",
+								Path:        "/account/{accountId}/cards",
+								Description: "Get account cards",
+								Alias:       "get",
+								Params:      []spec.Param{{Name: "accountId", Type: "string", Required: true, Positional: true}},
+							},
+						},
+					},
+					"statements": {
+						Description: "Manage statements",
+						Endpoints: map[string]spec.Endpoint{
+							"get-account": {
+								Method:      "GET",
+								Path:        "/account/{accountId}/statements",
+								Description: "Get account statements",
+								Alias:       "get",
+								Params:      []spec.Param{{Name: "accountId", Type: "string", Required: true, Positional: true}},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "promsub-pp-cli")
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	promotedSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "promoted_account.go"))
+	require.NoError(t, err)
+	assert.Contains(t, string(promotedSrc), "newAccountCardsCmd(flags)")
+	assert.Contains(t, string(promotedSrc), "newAccountStatementsCmd(flags)")
+	assert.NotContains(t, string(promotedSrc), "newAccountCardsGetAccountCmd(flags)")
+	assert.NotContains(t, string(promotedSrc), "newAccountStatementsGetAccountCmd(flags)")
+
+	cardsSrc, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "account_cards_get-account.go"))
+	require.NoError(t, err)
+	assert.Contains(t, string(cardsSrc), `Example: "  promsub-pp-cli account cards get-account `)
+	assert.NotContains(t, string(cardsSrc), `Example: "  promsub-pp-cli account get-account `)
+}
+
+func TestExampleLineUsesRenderedCommandAndFlagNames(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("example-render")
+	g := New(apiSpec, t.TempDir())
+
+	endpoint := spec.Endpoint{
+		Method: "POST",
+		Path:   "/account/{accountId}/request-send-money",
+		Params: []spec.Param{
+			{Name: "accountId", Type: "string", Required: true, Positional: true},
+		},
+		Body: []spec.Param{
+			{Name: "idempotencyKey", Type: "string", Required: true},
+		},
+	}
+
+	got := g.exampleLine("account request-send-money", "request_send_money", endpoint)
+
+	assert.Contains(t, got, "example-render-pp-cli account request-send-money request-send-money")
+	assert.Contains(t, got, "--idempotency-key your-token-here")
+	assert.NotContains(t, got, "request_send_money")
+	assert.NotContains(t, got, "--idempotencyKey")
+}
+
+func TestDetectAgentMoneyWorkflowFromGenericMoneyMovementShape(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("treasury")
+	apiSpec.Resources = map[string]spec.Resource{
+		"account": {
+			Description: "Manage accounts",
+			Endpoints: map[string]spec.Endpoint{
+				"get": {
+					Method:      "GET",
+					Path:        "/account/{accountId}",
+					Description: "Get account",
+					Params:      []spec.Param{{Name: "accountId", Type: "string", Required: true, Positional: true}},
+				},
+			},
+			SubResources: map[string]spec.Resource{
+				"request-send-money": {
+					Description: "Manage send-money requests",
+					Endpoints: map[string]spec.Endpoint{
+						"request_send_money": {
+							Method:      "POST",
+							Path:        "/account/{accountId}/request-send-money",
+							Description: "Create send-money request",
+							Alias:       "create",
+							Params:      []spec.Param{{Name: "accountId", Type: "string", Required: true, Positional: true}},
+							Body: []spec.Param{
+								{Name: "amount", Type: "number", Required: true},
+								{Name: "recipientId", Type: "string", Required: true},
+								{Name: "paymentMethod", Type: "string", Required: true},
+								{Name: "idempotencyKey", Type: "string", Required: true},
+							},
+						},
+					},
+				},
+				"transactions": {
+					Description: "Manage transactions",
+					Endpoints: map[string]spec.Endpoint{
+						"create": {
+							Method:      "POST",
+							Path:        "/account/{accountId}/transactions",
+							Description: "Create transaction",
+							Params:      []spec.Param{{Name: "accountId", Type: "string", Required: true, Positional: true}},
+							Body: []spec.Param{
+								{Name: "amount", Type: "number", Required: true},
+								{Name: "recipientId", Type: "string", Required: true},
+								{Name: "paymentMethod", Type: "string", Required: true},
+								{Name: "idempotencyKey", Type: "string", Required: true},
+								{Name: "externalMemo", Type: "string"},
+								{Name: "purpose", Type: "object"},
+							},
+						},
+					},
+				},
+			},
+		},
+		"transfer": {
+			Description: "Move funds between accounts",
+			Endpoints: map[string]spec.Endpoint{
+				"create": {
+					Method:      "POST",
+					Path:        "/transfer",
+					Description: "Create transfer",
+					Body: []spec.Param{
+						{Name: "sourceAccountId", Type: "string", Required: true},
+						{Name: "destinationAccountId", Type: "string", Required: true},
+						{Name: "amount", Type: "number", Required: true},
+						{Name: "idempotencyKey", Type: "string", Required: true},
+					},
+				},
+			},
+		},
+	}
+
+	workflow := detectAgentMoneyWorkflow(apiSpec, map[string]string{"transfer": "create"})
+
+	require.NotNil(t, workflow.Payment)
+	assert.Equal(t, []string{"account", "transactions", "create"}, workflow.Payment.CommandPath)
+	assert.True(t, workflow.Payment.HasAccountIDPosition)
+	assert.Equal(t, "amount", workflow.Payment.AmountFlag)
+	assert.Equal(t, "recipient-id", workflow.Payment.RecipientIDFlag)
+	assert.Equal(t, "payment-method", workflow.Payment.PaymentMethodFlag)
+	assert.Equal(t, "idempotency-key", workflow.Payment.IdempotencyKeyFlag)
+	assert.Equal(t, "external-memo", workflow.Payment.ExternalMemoFlag)
+	assert.Equal(t, "purpose", workflow.Payment.PurposeFlag)
+
+	require.NotNil(t, workflow.Request)
+	assert.Equal(t, []string{"account", "request-send-money", "create"}, workflow.Request.CommandPath,
+		"workflow plans should use generated endpoint aliases when available")
+
+	require.NotNil(t, workflow.Transfer)
+	assert.Equal(t, []string{"transfer"}, workflow.Transfer.CommandPath,
+		"promoted single-endpoint resources must emit the actual registered command path")
+	assert.Equal(t, "source-account-id", workflow.Transfer.SourceAccountIDFlag)
+	assert.Equal(t, "destination-account-id", workflow.Transfer.DestinationAccountIDFlag)
+	assert.True(t, workflow.Enabled())
+}
+
+func TestDetectAgentMoneyWorkflowRejectsIncompleteExecutablePlans(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("treasury")
+	apiSpec.Resources = map[string]spec.Resource{
+		"organizations": {
+			Description: "Manage organization payments",
+			Endpoints: map[string]spec.Endpoint{
+				"create-payment": {
+					Method:      "POST",
+					Path:        "/organizations/{organizationId}/payments",
+					Description: "Create payment",
+					Params:      []spec.Param{{Name: "organizationId", Type: "string", Required: true, Positional: true}},
+					Body: []spec.Param{
+						{Name: "amount", Type: "number", Required: true},
+						{Name: "recipientId", Type: "string", Required: true},
+						{Name: "paymentMethod", Type: "string", Required: true},
+						{Name: "idempotencyKey", Type: "string", Required: true},
+					},
+				},
+			},
+		},
+		"payments": {
+			Description: "Manage payments",
+			Endpoints: map[string]spec.Endpoint{
+				"create": {
+					Method:      "POST",
+					Path:        "/payments",
+					Description: "Create payment",
+					Body: []spec.Param{
+						{Name: "amount", Type: "number", Required: true},
+						{Name: "recipientId", Type: "string", Required: true},
+						{Name: "paymentMethod", Type: "string", Required: true},
+						{Name: "idempotencyKey", Type: "string", Required: true},
+						{Name: "currency", Type: "string", Required: true},
+					},
+				},
+			},
+		},
+	}
+
+	workflow := detectAgentMoneyWorkflow(apiSpec, nil)
+
+	assert.False(t, workflow.Enabled(), "payment-plan must not emit execute commands that omit required positionals or body flags")
+}
+
+func TestDetectAgentMoneyWorkflowTracksTransferPositionalsAndIntegerAmounts(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("treasury")
+	apiSpec.Resources = map[string]spec.Resource{
+		"account": {
+			Description: "Manage accounts",
+			SubResources: map[string]spec.Resource{
+				"transfers": {
+					Description: "Move funds between account ledgers",
+					Endpoints: map[string]spec.Endpoint{
+						"create": {
+							Method: "POST",
+							Path:   "/account/{accountId}/transfers",
+							Params: []spec.Param{{Name: "accountId", Type: "string", Required: true, Positional: true}},
+							Body: []spec.Param{
+								{Name: "sourceAccountId", Type: "string", Required: true},
+								{Name: "destinationAccountId", Type: "string", Required: true},
+								{Name: "amount", Type: "integer", Required: true},
+								{Name: "idempotencyKey", Type: "string", Required: true},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	workflow := detectAgentMoneyWorkflow(apiSpec, nil)
+
+	require.NotNil(t, workflow.Transfer)
+	assert.True(t, workflow.Transfer.HasAccountIDPosition)
+	assert.True(t, workflow.Transfer.AmountInteger)
+}
+
+func TestGeneratedOutput_AgentMoneyWorkflowPaymentPlan(t *testing.T) {
+	t.Parallel()
+
+	apiSpec := minimalSpec("treasury")
+	apiSpec.Resources["account"] = spec.Resource{
+		Description: "Manage accounts",
+		Endpoints: map[string]spec.Endpoint{
+			"get": {
+				Method:      "GET",
+				Path:        "/account/{accountId}",
+				Description: "Get account",
+				Params:      []spec.Param{{Name: "accountId", Type: "string", Required: true, Positional: true}},
+			},
+		},
+		SubResources: map[string]spec.Resource{
+			"transactions": {
+				Description: "Manage transactions",
+				Endpoints: map[string]spec.Endpoint{
+					"create": {
+						Method:      "POST",
+						Path:        "/account/{accountId}/transactions",
+						Description: "Create transaction",
+						Params:      []spec.Param{{Name: "accountId", Type: "string", Required: true, Positional: true}},
+						Body: []spec.Param{
+							{Name: "amount", Type: "number", Required: true},
+							{Name: "recipientId", Type: "string", Required: true},
+							{Name: "paymentMethod", Type: "string", Required: true},
+							{Name: "idempotencyKey", Type: "string", Required: true},
+							{Name: "purpose", Type: "object"},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), "treasury-pp-cli")
+	require.NoError(t, New(apiSpec, outputDir).Generate())
+
+	workflowGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "channel_workflow.go"))
+	require.NoError(t, err)
+	workflowSrc := string(workflowGo)
+	assert.Contains(t, workflowSrc, "func newWorkflowPaymentPlanCmd(flags *rootFlags) *cobra.Command")
+	assert.Contains(t, workflowSrc, `Use:   "payment-plan"`)
+	assert.Contains(t, workflowSrc, `base = []string{"treasury-pp-cli", "account", "transactions", "create"}`)
+	assert.Contains(t, workflowSrc, `"dry_run_command": append(append([]string{}, base...), "--dry-run", "--agent")`)
+	assert.Contains(t, workflowSrc, `"execute_command": append([]string{}, base...)`)
+
+	runGoCommand(t, outputDir, "mod", "tidy")
+	runGoCommand(t, outputDir, "build", "./...")
+
+	binaryPath := filepath.Join(outputDir, "treasury-pp-cli")
+	runGoCommand(t, outputDir, "build", "-o", binaryPath, "./cmd/treasury-pp-cli")
+
+	cmd := exec.Command(binaryPath, "workflow", "payment-plan",
+		"--kind", "payment",
+		"--account-id", "acct_123",
+		"--recipient-id", "rec_123",
+		"--amount", "25",
+		"--payment-method", "ach",
+		"--idempotency-key", "idem",
+		"--json",
+	)
+	out, err := cmd.CombinedOutput()
+	require.NoError(t, err, string(out))
+
+	var plan map[string]any
+	require.NoError(t, json.Unmarshal(out, &plan))
+	assert.Equal(t, "payment", plan["kind"])
+	assert.Equal(t, true, plan["read_only"])
+	assert.Equal(t, true, plan["requires_approval"])
+	assert.Equal(t, []any{"treasury-pp-cli", "account", "transactions", "create", "acct_123", "--amount", "25.00", "--recipient-id", "rec_123", "--payment-method", "ach", "--idempotency-key", "idem", "--dry-run", "--agent"}, plan["dry_run_command"])
+	assert.Equal(t, []any{"treasury-pp-cli", "account", "transactions", "create", "acct_123", "--amount", "25.00", "--recipient-id", "rec_123", "--payment-method", "ach", "--idempotency-key", "idem"}, plan["execute_command"])
+
+	cmd = exec.Command(binaryPath, "workflow", "payment-plan",
+		"--kind", "payment",
+		"--account-id", "acct_123",
+		"--recipient-id", "rec_123",
+		"--amount", "25",
+		"--payment-method", "domesticWire",
+		"--idempotency-key", "idem",
+	)
+	out, err = cmd.CombinedOutput()
+	require.Error(t, err)
+	assert.Contains(t, string(out), "--purpose is required for domesticWire payment plans")
+}
+
 func TestGeneratedOutput_PromotedCommandCompiles(t *testing.T) {
 	t.Parallel()
 
@@ -6801,7 +7158,7 @@ func TestTemplatesEmitReadOnlyAnnotation(t *testing.T) {
 		{"api_discovery.go.tmpl", 1, "walks cobra tree, prints help"},
 		{"tail.go.tmpl", 1, "polls API GETs, NDJSON to stdout"},
 		{"jobs.go.tmpl", 2, "list and get; prune is omitted (mutates ledger)"},
-		{"channel_workflow.go.tmpl", 1, "status only; workflow parent and archive omitted"},
+		{"channel_workflow.go.tmpl", 2, "status and generated payment-plan are read-only; workflow parent and archive omitted"},
 	}
 
 	for _, tc := range cases {
diff --git a/internal/generator/templates/auth_simple.go.tmpl b/internal/generator/templates/auth_simple.go.tmpl
index 73d923ed..8c183341 100644
--- a/internal/generator/templates/auth_simple.go.tmpl
+++ b/internal/generator/templates/auth_simple.go.tmpl
@@ -88,7 +88,7 @@ func newAuthSetTokenCmd(flags *rootFlags) *cobra.Command {
 	return &cobra.Command{
 		Use:   "set-token <token>",
 		Short: "Save an API token to the config file",
-		Example: "  {{.Name}}-pp-cli auth set-token sk_live_abc123",
+		Example: "  {{.Name}}-pp-cli auth set-token YOUR_TOKEN_HERE",
 		Args:  cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cfg, err := config.Load(flags.configPath)
diff --git a/internal/generator/templates/channel_workflow.go.tmpl b/internal/generator/templates/channel_workflow.go.tmpl
index 59d507b5..98ea000f 100644
--- a/internal/generator/templates/channel_workflow.go.tmpl
+++ b/internal/generator/templates/channel_workflow.go.tmpl
@@ -4,8 +4,15 @@
 package cli
 
 import (
+{{- if .AgentMoneyWorkflow.Enabled}}
+	"crypto/rand"
+	"encoding/hex"
+{{- end}}
 	"encoding/json"
 	"fmt"
+{{- if .AgentMoneyWorkflow.Enabled}}
+	"strings"
+{{- end}}
 	"time"
 
 	"{{modulePath}}/internal/store"
@@ -20,10 +27,218 @@ func newWorkflowCmd(flags *rootFlags) *cobra.Command {
 
 	cmd.AddCommand(newWorkflowArchiveCmd(flags))
 	cmd.AddCommand(newWorkflowStatusCmd(flags))
+{{- if .AgentMoneyWorkflow.Enabled}}
+	cmd.AddCommand(newWorkflowPaymentPlanCmd(flags))
+{{- end}}
 
 	return cmd
 }
 
+{{- if .AgentMoneyWorkflow.Enabled}}
+func newWorkflowPaymentPlanCmd(flags *rootFlags) *cobra.Command {
+	var kind string
+	var accountID string
+	var recipientID string
+	var sourceAccountID string
+	var destinationAccountID string
+	var amount float64
+	var paymentMethod string
+	var idempotencyKey string
+	var note string
+	var externalMemo string
+	var purpose string
+
+	cmd := &cobra.Command{
+		Use:   "payment-plan",
+		Short: "Build a safe, executable money-movement plan without sending it",
+		Long: `Build a structured money-movement plan for agents. The command never calls
+the API and never moves money. It emits a dry-run command plus the separate
+execute command an operator can approve later.`,
+		Example: `  {{.Name}}-pp-cli workflow payment-plan --kind payment --account-id acct_123 --recipient-id rec_123 --amount 25 --payment-method ach --agent
+  {{.Name}}-pp-cli workflow payment-plan --kind transfer --source-account-id acct_src --destination-account-id acct_dst --amount 25 --agent`,
+		Annotations: map[string]string{"mcp:read-only": "true"},
+		RunE: func(cmd *cobra.Command, args []string) error {
+			kind = strings.ToLower(strings.TrimSpace(kind))
+			if idempotencyKey == "" {
+				idempotencyKey = "agent-" + randomHex(12)
+			}
+			if amount <= 0 {
+				return fmt.Errorf("--amount must be greater than 0")
+			}
+
+			var base []string
+			body := map[string]any{
+				"amount": amount,
+				"idempotencyKey": idempotencyKey,
+			}
+			if note != "" {
+				body["note"] = note
+			}
+
+			switch kind {
+{{- with .AgentMoneyWorkflow.Payment}}
+			case "payment", "send":
+				if accountID == "" || recipientID == "" || paymentMethod == "" {
+					return fmt.Errorf("--account-id, --recipient-id, and --payment-method are required for --kind payment")
+				}
+				if strings.EqualFold(paymentMethod, "domesticWire") && purpose == "" {
+					return fmt.Errorf("--purpose is required for domesticWire payment plans")
+				}
+				amountArg, err := workflowAmountArg(amount, {{.AmountInteger}})
+				if err != nil {
+					return err
+				}
+				body["recipientId"] = recipientID
+				body["paymentMethod"] = paymentMethod
+				if externalMemo != "" {
+					body["externalMemo"] = externalMemo
+				}
+				if purpose != "" {
+					body["purpose"] = map[string]any{"simple": purpose}
+				}
+				base = []string{"{{$.Name}}-pp-cli"{{range .CommandPath}}, "{{.}}"{{end}}}
+{{- if .HasAccountIDPosition}}
+				base = append(base, accountID)
+{{- end}}
+				base = append(base,
+					"--{{.AmountFlag}}", amountArg,
+					"--{{.RecipientIDFlag}}", recipientID,
+					"--{{.PaymentMethodFlag}}", paymentMethod,
+					"--{{.IdempotencyKeyFlag}}", idempotencyKey,
+				)
+{{- if .NoteFlag}}
+				if note != "" {
+					base = append(base, "--{{.NoteFlag}}", note)
+				}
+{{- end}}
+{{- if .ExternalMemoFlag}}
+				if externalMemo != "" {
+					base = append(base, "--{{.ExternalMemoFlag}}", externalMemo)
+				}
+{{- end}}
+{{- if .PurposeFlag}}
+				if purpose != "" {
+					base = append(base, "--{{.PurposeFlag}}", fmt.Sprintf(`{"simple":%q}`, purpose))
+				}
+{{- end}}
+{{- end}}
+{{- with .AgentMoneyWorkflow.Request}}
+			case "request":
+				if accountID == "" || recipientID == "" || paymentMethod == "" {
+					return fmt.Errorf("--account-id, --recipient-id, and --payment-method are required for --kind request")
+				}
+				amountArg, err := workflowAmountArg(amount, {{.AmountInteger}})
+				if err != nil {
+					return err
+				}
+				body["recipientId"] = recipientID
+				body["paymentMethod"] = paymentMethod
+				if externalMemo != "" {
+					body["externalMemo"] = externalMemo
+				}
+				base = []string{"{{$.Name}}-pp-cli"{{range .CommandPath}}, "{{.}}"{{end}}}
+{{- if .HasAccountIDPosition}}
+				base = append(base, accountID)
+{{- end}}
+				base = append(base,
+					"--{{.AmountFlag}}", amountArg,
+					"--{{.RecipientIDFlag}}", recipientID,
+					"--{{.PaymentMethodFlag}}", paymentMethod,
+					"--{{.IdempotencyKeyFlag}}", idempotencyKey,
+				)
+{{- if .NoteFlag}}
+				if note != "" {
+					base = append(base, "--{{.NoteFlag}}", note)
+				}
+{{- end}}
+{{- if .ExternalMemoFlag}}
+				if externalMemo != "" {
+					base = append(base, "--{{.ExternalMemoFlag}}", externalMemo)
+				}
+{{- end}}
+{{- end}}
+{{- with .AgentMoneyWorkflow.Transfer}}
+			case "transfer":
+				if sourceAccountID == "" || destinationAccountID == "" {
+					return fmt.Errorf("--source-account-id and --destination-account-id are required for --kind transfer")
+				}
+{{- if .HasAccountIDPosition}}
+				if accountID == "" {
+					return fmt.Errorf("--account-id is required for --kind transfer")
+				}
+{{- end}}
+				amountArg, err := workflowAmountArg(amount, {{.AmountInteger}})
+				if err != nil {
+					return err
+				}
+				body["sourceAccountId"] = sourceAccountID
+				body["destinationAccountId"] = destinationAccountID
+				base = []string{"{{$.Name}}-pp-cli"{{range .CommandPath}}, "{{.}}"{{end}}}
+{{- if .HasAccountIDPosition}}
+				base = append(base, accountID)
+{{- end}}
+				base = append(base,
+					"--{{.SourceAccountIDFlag}}", sourceAccountID,
+					"--{{.DestinationAccountIDFlag}}", destinationAccountID,
+					"--{{.AmountFlag}}", amountArg,
+					"--{{.IdempotencyKeyFlag}}", idempotencyKey,
+				)
+{{- if .NoteFlag}}
+				if note != "" {
+					base = append(base, "--{{.NoteFlag}}", note)
+				}
+{{- end}}
+{{- end}}
+			default:
+				return fmt.Errorf("--kind must be one of:{{if .AgentMoneyWorkflow.Payment}} payment{{end}}{{if .AgentMoneyWorkflow.Request}} request{{end}}{{if .AgentMoneyWorkflow.Transfer}} transfer{{end}}")
+			}
+
+			return printJSONFiltered(cmd.OutOrStdout(), map[string]any{
+				"kind": kind,
+				"body": body,
+				"dry_run_command": append(append([]string{}, base...), "--dry-run", "--agent"),
+				"execute_command": append([]string{}, base...),
+				"idempotency_key": idempotencyKey,
+				"requires_approval": true,
+				"read_only": true,
+				"created_at": time.Now().UTC().Format(time.RFC3339),
+			}, flags)
+		},
+	}
+
+	cmd.Flags().StringVar(&kind, "kind", "{{if .AgentMoneyWorkflow.Payment}}payment{{else if .AgentMoneyWorkflow.Request}}request{{else}}transfer{{end}}", "Money movement kind: payment, request, or transfer")
+	cmd.Flags().StringVar(&accountID, "account-id", "", "Account ID for account-scoped payment/request flows")
+	cmd.Flags().StringVar(&recipientID, "recipient-id", "", "Recipient ID for payment/request flows")
+	cmd.Flags().StringVar(&sourceAccountID, "source-account-id", "", "Source account ID for transfer flows")
+	cmd.Flags().StringVar(&destinationAccountID, "destination-account-id", "", "Destination account ID for transfer flows")
+	cmd.Flags().Float64Var(&amount, "amount", 0, "Dollar amount")
+	cmd.Flags().StringVar(&paymentMethod, "payment-method", "", "Payment method for payment/request flows")
+	cmd.Flags().StringVar(&idempotencyKey, "idempotency-key", "", "Idempotency key; generated when omitted")
+	cmd.Flags().StringVar(&note, "note", "", "Internal note")
+	cmd.Flags().StringVar(&externalMemo, "external-memo", "", "External memo")
+	cmd.Flags().StringVar(&purpose, "purpose", "", "Simple purpose text for domestic wires when supported")
+	return cmd
+}
+
+func randomHex(n int) string {
+	buf := make([]byte, n)
+	if _, err := rand.Read(buf); err != nil {
+		return fmt.Sprintf("%d", time.Now().UnixNano())
+	}
+	return hex.EncodeToString(buf)
+}
+
+func workflowAmountArg(amount float64, integer bool) (string, error) {
+	if !integer {
+		return fmt.Sprintf("%.2f", amount), nil
+	}
+	if amount != float64(int64(amount)) {
+		return "", fmt.Errorf("--amount must be a whole number for this payment endpoint")
+	}
+	return fmt.Sprintf("%.0f", amount), nil
+}
+{{- end}}
+
 func newWorkflowArchiveCmd(flags *rootFlags) *cobra.Command {
 	var dbPath string
 	var full bool
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index 211c319b..dc13877c 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -300,17 +300,11 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
 {{- end}}
 {{- end}}
 {{- range $subName, $sub := .Resource.SubResources}}
-{{- if eq (len $sub.Endpoints) 1}}
-{{- range $eName, $_ := $sub.Endpoints}}
-	cmd.AddCommand(new{{camel $.FuncPrefix}}{{camel $subName}}{{camel $eName}}Cmd(flags))
-{{- end}}
-{{- else}}
 	{
 		sub := new{{camel $.FuncPrefix}}{{camel $subName}}Cmd(flags)
 		sub.Hidden = false // unhide: the raw parent is hidden but these are useful under the promoted command
 		cmd.AddCommand(sub)
 	}
-{{- end}}
 {{- end}}
 
 	return cmd
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index cb59b978..fb2fdfca 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"maps"
 	"net/url"
 	"os"
 	"regexp"
@@ -2250,33 +2251,34 @@ func mapRequestBody(requestBodyRef *openapi3.RequestBodyRef, method, path string
 		}
 		seenCamelNames[camelName] = true
 		schema := schemaRefValue(properties[name])
-		if isComplexBodyFieldSchema(schema) {
-			warnf("skipping body field %q: complex type not supported as CLI flag", name)
-			continue
-		}
+		paramSchema := bodyParamSchema(schema)
 		description := schemaDescription(schema)
+		if description == "" {
+			description = schemaDescription(paramSchema)
+		}
 		if description == "" {
 			description = humanizeFieldName(name)
 		}
 		param := spec.Param{
 			Name:        name,
-			Type:        mapSchemaType(schema),
+			Type:        mapSchemaType(paramSchema),
 			Required:    isRequired(required, name),
 			Description: description,
-			Enum:        schemaEnum(schema),
-			Format:      schemaFormat(schema),
+			Fields:      mapBodyFields(paramSchema),
+			Enum:        schemaEnum(paramSchema),
+			Format:      schemaFormat(paramSchema),
 		}
-		if schema != nil && schema.Default != nil {
-			param.Default = schema.Default
+		if paramSchema != nil && paramSchema.Default != nil {
+			param.Default = paramSchema.Default
 		}
 		// For array types, propagate item-level enum as a Fields entry
 		// so downstream consumers (profiler) can access it.
-		if schema != nil && schema.Type != nil && schema.Type.Is(openapi3.TypeArray) &&
-			schema.Items != nil && schema.Items.Value != nil && len(schema.Items.Value.Enum) > 0 {
+		if paramSchema != nil && paramSchema.Type != nil && paramSchema.Type.Is(openapi3.TypeArray) &&
+			paramSchema.Items != nil && paramSchema.Items.Value != nil && len(paramSchema.Items.Value.Enum) > 0 {
 			param.Fields = []spec.Param{{
 				Name: "items",
 				Type: "string",
-				Enum: schemaEnum(schema.Items.Value),
+				Enum: schemaEnum(paramSchema.Items.Value),
 			}}
 		}
 		body = append(body, param)
@@ -2285,6 +2287,104 @@ func mapRequestBody(requestBodyRef *openapi3.RequestBodyRef, method, path string
 	return body
 }
 
+func bodyParamSchema(schema *openapi3.Schema) *openapi3.Schema {
+	if schema == nil || len(schema.AllOf) == 0 {
+		return schema
+	}
+	if schema.Type != nil || len(schema.Properties) > 0 || schema.Items != nil {
+		return schema
+	}
+
+	var mergedObject *openapi3.Schema
+	required := map[string]struct{}{}
+	for _, candidate := range schema.AllOf {
+		value := schemaRefValue(candidate)
+		if value == nil {
+			continue
+		}
+		if isObjectSchema(value) {
+			if mergedObject == nil {
+				mergedObject = &openapi3.Schema{
+					Type:       &openapi3.Types{openapi3.TypeObject},
+					Properties: map[string]*openapi3.SchemaRef{},
+				}
+			}
+			if mergedObject.Description == "" {
+				mergedObject.Description = value.Description
+			}
+			maps.Copy(mergedObject.Properties, value.Properties)
+			for _, name := range value.Required {
+				required[name] = struct{}{}
+			}
+			continue
+		}
+		if mergedObject == nil && (value.Type != nil || value.Items != nil) {
+			return value
+		}
+	}
+	if mergedObject != nil {
+		for name := range required {
+			mergedObject.Required = append(mergedObject.Required, name)
+		}
+		sort.Strings(mergedObject.Required)
+		return mergedObject
+	}
+	return schema
+}
+
+func mapBodyFields(schema *openapi3.Schema) []spec.Param {
+	return mapBodyFieldsDepth(schema, map[*openapi3.Schema]struct{}{}, 0)
+}
+
+const maxBodyFieldDepth = 8
+
+func mapBodyFieldsDepth(schema *openapi3.Schema, visited map[*openapi3.Schema]struct{}, depth int) []spec.Param {
+	if !isObjectSchema(schema) || len(schema.Properties) == 0 {
+		return nil
+	}
+	if depth >= maxBodyFieldDepth {
+		return nil
+	}
+	if _, ok := visited[schema]; ok {
+		return nil
+	}
+	visited[schema] = struct{}{}
+	defer delete(visited, schema)
+
+	names := make([]string, 0, len(schema.Properties))
+	for name := range schema.Properties {
+		names = append(names, name)
+	}
+	sort.Strings(names)
+
+	required := map[string]struct{}{}
+	for _, name := range schema.Required {
+		required[name] = struct{}{}
+	}
+
+	fields := make([]spec.Param, 0, len(names))
+	for _, name := range names {
+		fieldSchema := bodyParamSchema(schemaRefValue(schema.Properties[name]))
+		description := schemaDescription(schemaRefValue(schema.Properties[name]))
+		if description == "" {
+			description = schemaDescription(fieldSchema)
+		}
+		if description == "" {
+			description = humanizeFieldName(name)
+		}
+		fields = append(fields, spec.Param{
+			Name:        name,
+			Type:        mapSchemaType(fieldSchema),
+			Required:    isRequired(required, name),
+			Description: description,
+			Fields:      mapBodyFieldsDepth(fieldSchema, visited, depth+1),
+			Enum:        schemaEnum(fieldSchema),
+			Format:      schemaFormat(fieldSchema),
+		})
+	}
+	return fields
+}
+
 func collectAllOfProperties(
 	schemaRef *openapi3.SchemaRef,
 	properties map[string]*openapi3.SchemaRef,
@@ -2744,6 +2844,8 @@ func mapSchemaType(schema *openapi3.Schema) string {
 		return "float"
 	case schema.Type.Includes(openapi3.TypeArray):
 		return "array"
+	case schema.Type.Includes(openapi3.TypeObject):
+		return "object"
 	case schema.Type.Includes(openapi3.TypeString):
 		return "string"
 	default:
@@ -2863,25 +2965,6 @@ func jsonAPIFlattenInto(schema *openapi3.Schema, properties map[string]*openapi3
 	}
 }
 
-func isComplexBodyFieldSchema(schema *openapi3.Schema) bool {
-	if isObjectSchema(schema) {
-		return true
-	}
-	if isArraySchema(schema) {
-		// Arrays with simple string/enum items are not truly complex —
-		// they can be represented as comma-separated flags and are needed
-		// by the profiler for search body construction.
-		if schema.Items != nil && schema.Items.Value != nil {
-			itemSchema := schema.Items.Value
-			if itemSchema.Type != nil && itemSchema.Type.Is(openapi3.TypeString) {
-				return false // simple string array, keep it
-			}
-		}
-		return true
-	}
-	return false
-}
-
 func schemaTypeName(schemaRef *openapi3.SchemaRef, fallback string) string {
 	if schemaRef == nil {
 		return toTypeName(fallback)
diff --git a/internal/openapi/parser_test.go b/internal/openapi/parser_test.go
index f0d6eb09..a9e47fa7 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -120,6 +120,149 @@ components:
 	assert.Equal(t, []string{"workspace", "collection"}, typeField.Enum)
 }
 
+func TestParseMapsAllOfRequestBodyFields(t *testing.T) {
+	t.Parallel()
+
+	parsed, err := Parse([]byte(`
+openapi: 3.0.3
+info:
+  title: Banking API
+  version: 1.0.0
+paths:
+  /account/{accountId}/transactions:
+    post:
+      operationId: createTransaction
+      parameters:
+        - name: accountId
+          in: path
+          required: true
+          schema:
+            type: string
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required: [amount, paymentMethod, recipientId, purpose]
+              properties:
+                amount:
+                  allOf:
+                    - $ref: "#/components/schemas/PositiveDollar"
+                  description: Amount of USD you want to send.
+                paymentMethod:
+                  allOf:
+                    - $ref: "#/components/schemas/PaymentMethod"
+                recipientId:
+                  allOf:
+                    - $ref: "#/components/schemas/RecipientId"
+                purpose:
+                  allOf:
+                    - $ref: "#/components/schemas/PaymentPurpose"
+                    - $ref: "#/components/schemas/PurposeMetadata"
+      responses:
+        "200":
+          description: ok
+          content:
+            application/json:
+              schema:
+                type: object
+components:
+  schemas:
+    PositiveDollar:
+      type: number
+      format: double
+      description: A positive dollar amount with at least 1 cent.
+    PaymentMethod:
+      type: string
+      enum: [ach, check, domesticWire]
+    RecipientId:
+      type: string
+      format: uuid
+    PaymentPurpose:
+      type: object
+      properties:
+        simple:
+          type: string
+    PurposeMetadata:
+      type: object
+      required: [memo]
+      properties:
+        memo:
+          type: string
+`))
+	require.NoError(t, err)
+
+	endpoint := findParsedEndpointByPath(t, parsed, "POST", "/account/{accountId}/transactions")
+	byName := map[string]spec.Param{}
+	for _, param := range endpoint.Body {
+		byName[param.Name] = param
+	}
+
+	assert.Equal(t, "float", byName["amount"].Type)
+	assert.Equal(t, "double", byName["amount"].Format)
+	assert.Equal(t, "string", byName["paymentMethod"].Type)
+	assert.Equal(t, []string{"ach", "check", "domesticWire"}, byName["paymentMethod"].Enum)
+	assert.Equal(t, "string", byName["recipientId"].Type)
+	assert.Equal(t, "uuid", byName["recipientId"].Format)
+	assert.Equal(t, "object", byName["purpose"].Type)
+	assert.Equal(t, []spec.Param{
+		{Name: "memo", Type: "string", Required: true, Description: "Memo"},
+		{Name: "simple", Type: "string", Description: "Simple"},
+	}, byName["purpose"].Fields)
+}
+
+func TestParseRecursiveRequestBodyFieldsStopsAtCycle(t *testing.T) {
+	t.Parallel()
+
+	parsed, err := Parse([]byte(`
+openapi: 3.0.3
+info:
+  title: Recursive API
+  version: 1.0.0
+paths:
+  /nodes:
+    post:
+      operationId: createNode
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              required: [node]
+              properties:
+                node:
+                  $ref: "#/components/schemas/Node"
+      responses:
+        "200":
+          description: ok
+components:
+  schemas:
+    Node:
+      type: object
+      required: [name]
+      properties:
+        name:
+          type: string
+        child:
+          $ref: "#/components/schemas/Node"
+`))
+	require.NoError(t, err)
+
+	endpoint := findParsedEndpointByPath(t, parsed, "POST", "/nodes")
+	require.Len(t, endpoint.Body, 1)
+	require.Equal(t, "node", endpoint.Body[0].Name)
+	assert.Equal(t, "object", endpoint.Body[0].Type)
+
+	fieldsByName := map[string]spec.Param{}
+	for _, field := range endpoint.Body[0].Fields {
+		fieldsByName[field.Name] = field
+	}
+	assert.Equal(t, "string", fieldsByName["name"].Type)
+	assert.Equal(t, "object", fieldsByName["child"].Type)
+	assert.Empty(t, fieldsByName["child"].Fields)
+}
+
 func TestParseStytchOpenAPI(t *testing.T) {
 	t.Parallel()
 
@@ -2989,3 +3132,23 @@ paths:
 	require.NoError(t, err)
 	assert.Equal(t, "https://global.example.com", parsed.BaseURL)
 }
+
+func findParsedEndpointByPath(t *testing.T, parsed *spec.APISpec, method, path string) spec.Endpoint {
+	t.Helper()
+	for _, resource := range parsed.Resources {
+		for _, endpoint := range resource.Endpoints {
+			if endpoint.Method == method && endpoint.Path == path {
+				return endpoint
+			}
+		}
+		for _, sub := range resource.SubResources {
+			for _, endpoint := range sub.Endpoints {
+				if endpoint.Method == method && endpoint.Path == path {
+					return endpoint
+				}
+			}
+		}
+	}
+	t.Fatalf("endpoint %s %s not found", method, path)
+	return spec.Endpoint{}
+}
diff --git a/internal/pipeline/fullrun.go b/internal/pipeline/fullrun.go
index e9427b3f..e22a1e81 100644
--- a/internal/pipeline/fullrun.go
+++ b/internal/pipeline/fullrun.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/mvanhorn/cli-printing-press/v3/internal/artifacts"
 	"github.com/mvanhorn/cli-printing-press/v3/internal/llmpolish"
 	"github.com/mvanhorn/cli-printing-press/v3/internal/naming"
 	"gopkg.in/yaml.v3"
@@ -305,6 +306,7 @@ func copySpecToOutput(specFlag, specURL, outputDir string) error {
 	if err != nil {
 		return fmt.Errorf("converting spec to JSON: %w", err)
 	}
+	data = artifacts.RedactArchivedSpecSecrets(data)
 	dst := filepath.Join(outputDir, "spec.json")
 	if err := os.WriteFile(dst, data, 0o644); err != nil {
 		return fmt.Errorf("writing %s: %w", dst, err)
diff --git a/internal/pipeline/live_dogfood.go b/internal/pipeline/live_dogfood.go
index a504b300..6fb42136 100644
--- a/internal/pipeline/live_dogfood.go
+++ b/internal/pipeline/live_dogfood.go
@@ -37,6 +37,9 @@ const (
 // 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"
+const reasonMutatingDryRunOnly = "mutating command dry-run only"
+const reasonNoLiveSignal = "no live happy/json pass; credential-unavailable skips cannot certify acceptance"
+const reasonUnavailableRunnerCredentials = "unavailable for runner credentials"
 
 type LiveDogfoodOptions struct {
 	CLIDir              string
@@ -187,6 +190,7 @@ func discoverLiveDogfoodCommands(binaryPath string) ([]liveDogfoodCommand, error
 
 var liveDogfoodFrameworkSkip = map[string]bool{
 	"agent-context": true,
+	"auth":          true,
 	"completion":    true,
 	"help":          true,
 	"version":       true,
@@ -223,10 +227,36 @@ var mutatingVerbs = map[string]bool{
 	"create": true, "add": true, "new": true,
 	"update": true, "patch": true, "edit": true,
 	"set": true, "modify": true, "replace": true,
+	"post": true, "put": true, "send": true, "submit": true,
+	"transfer": true, "cancel": true, "freeze": true, "unfreeze": true,
 }
 
 func isMutatingLeaf(name string) bool {
-	return mutatingVerbs[name]
+	for _, token := range commandNameTokens(name) {
+		if mutatingVerbs[token] {
+			return true
+		}
+	}
+	return false
+}
+
+func liveDogfoodCommandMutates(command liveDogfoodCommand) bool {
+	if annotationIsTrueValue(command.Annotations[mcpReadOnlyAnnotation]) {
+		return false
+	}
+	if method := strings.ToUpper(strings.TrimSpace(command.Annotations[endpointMethodAnnotation])); method != "" {
+		return method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE"
+	}
+	if len(command.Path) == 0 {
+		return false
+	}
+	return isMutatingLeaf(command.Path[len(command.Path)-1])
+}
+
+func commandNameTokens(name string) []string {
+	return strings.FieldsFunc(strings.ToLower(name), func(r rune) bool {
+		return r < 'a' || r > 'z'
+	})
 }
 
 // companionCache is run-scoped: per-RunLiveDogfood maps keyed by the full
@@ -640,8 +670,8 @@ func runLiveDogfoodCommand(command liveDogfoodCommand, ctx resolveCtx) []LiveDog
 		return results
 	}
 
-	leaf := command.Path[len(command.Path)-1]
-	useDryRun := isMutatingLeaf(leaf) && commandSupportsDryRun(command.Help)
+	mutating := liveDogfoodCommandMutates(command)
+	useDryRun := mutating && commandSupportsDryRun(command.Help)
 
 	resolvedArgs, resolveSkipped, resolveReason := resolveCommandPositionals(command, happyArgs, ctx)
 	if resolveSkipped {
@@ -662,6 +692,9 @@ func runLiveDogfoodCommand(command liveDogfoodCommand, ctx resolveCtx) []LiveDog
 		if happyRun.exitCode == 0 {
 			happyResult.Status = LiveDogfoodStatusPass
 			happyResult.Reason = ""
+		} else if liveDogfoodUnavailableForRunner(happyRun) {
+			happyResult.Status = LiveDogfoodStatusSkip
+			happyResult.Reason = reasonUnavailableRunnerCredentials
 		}
 		results = append(results, happyResult)
 
@@ -670,13 +703,16 @@ func runLiveDogfoodCommand(command liveDogfoodCommand, ctx resolveCtx) []LiveDog
 			jsonRun := runLiveDogfoodProcess(ctx.binaryPath, ctx.cliDir, jsonArgs, ctx.timeout)
 			jsonResult := liveDogfoodResult(commandName, LiveDogfoodTestJSON, jsonArgs, jsonRun)
 			if jsonRun.exitCode == 0 {
-				if !json.Valid([]byte(jsonRun.stdout)) {
+				if !validLiveDogfoodJSONOutput(jsonRun.stdout) {
 					jsonResult.Status = LiveDogfoodStatusFail
 					jsonResult.Reason = "invalid JSON"
 				} else {
 					jsonResult.Status = LiveDogfoodStatusPass
 					jsonResult.Reason = ""
 				}
+			} else if liveDogfoodUnavailableForRunner(jsonRun) {
+				jsonResult.Status = LiveDogfoodStatusSkip
+				jsonResult.Reason = reasonUnavailableRunnerCredentials
 			}
 			results = append(results, jsonResult)
 		} else {
@@ -690,7 +726,7 @@ func runLiveDogfoodCommand(command liveDogfoodCommand, ctx resolveCtx) []LiveDog
 		// Search-shape strategy is suppressed for mutating leaves so a
 		// `delete --query=...` mass-delete is never probed with the
 		// invalid-token sentinel against the live API.
-		isSearch := commandSupportsSearch(command.Help) && !isMutatingLeaf(leaf)
+		isSearch := commandSupportsSearch(command.Help) && !mutating
 		suppliedJSON := slices.Contains(flagNames, "json")
 
 		var errorArgs []string
@@ -745,16 +781,7 @@ func runLiveDogfoodCommand(command liveDogfoodCommand, ctx resolveCtx) []LiveDog
 		if resolveSkipped {
 			results = append(results, skippedLiveDogfoodResult(commandName, LiveDogfoodTestErrorReal, resolveReason))
 		} else {
-			errorRealRun := runLiveDogfoodProcess(ctx.binaryPath, ctx.cliDir, happyArgs, ctx.timeout)
-			errorRealResult := liveDogfoodResult(commandName, LiveDogfoodTestErrorReal, happyArgs, errorRealRun)
-			if errorRealRun.exitCode != 0 {
-				errorRealResult.Status = LiveDogfoodStatusPass
-				errorRealResult.Reason = ""
-			} else {
-				errorRealResult.Status = LiveDogfoodStatusFail
-				errorRealResult.Reason = "expected non-zero exit for placeholder body"
-			}
-			results = append(results, errorRealResult)
+			results = append(results, skippedLiveDogfoodResult(commandName, LiveDogfoodTestErrorReal, reasonMutatingDryRunOnly))
 		}
 	}
 
@@ -925,10 +952,7 @@ func isDestructiveAtAuth(annotations map[string]string, commandPath []string) bo
 }
 
 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 slices.ContainsFunc(commandNameTokens(s), func(token string) bool {
 		return destructiveAuthTerms[token]
 	})
 }
@@ -964,6 +988,33 @@ func commandSupportsJSON(help string) bool {
 	return slices.Contains(extractFlagNames(help), "json")
 }
 
+func validLiveDogfoodJSONOutput(stdout string) bool {
+	trimmed := strings.TrimSpace(stdout)
+	if trimmed == "" {
+		return false
+	}
+	if json.Valid([]byte(trimmed)) {
+		return true
+	}
+	for line := range strings.SplitSeq(trimmed, "\n") {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+		if !json.Valid([]byte(line)) {
+			return false
+		}
+	}
+	return true
+}
+
+func liveDogfoodUnavailableForRunner(run liveDogfoodRun) bool {
+	output := strings.ToLower(run.stdout + run.stderr)
+	return strings.Contains(output, "http 403") ||
+		strings.Contains(output, "permission denied") ||
+		strings.Contains(output, "your credentials are valid but lack access")
+}
+
 func commandSupportsDryRun(help string) bool {
 	return slices.Contains(extractFlagNames(help), "dry-run")
 }
@@ -1007,18 +1058,36 @@ func liveDogfoodUsageSuffix(help string) string {
 }
 
 func finalizeLiveDogfoodReport(report *LiveDogfoodReport) {
+	hasUnavailableRunnerSkip := false
+	hasLiveHappyOrJSONPass := false
 	for _, result := range report.Tests {
 		switch result.Status {
 		case LiveDogfoodStatusPass:
 			report.Passed++
 			report.MatrixSize++
+			if (result.Kind == LiveDogfoodTestHappy || result.Kind == LiveDogfoodTestJSON) && !slices.Contains(result.Args, "--dry-run") {
+				hasLiveHappyOrJSONPass = true
+			}
 		case LiveDogfoodStatusFail:
 			report.Failed++
 			report.MatrixSize++
 		default:
 			report.Skipped++
+			if result.Reason == reasonUnavailableRunnerCredentials {
+				hasUnavailableRunnerSkip = true
+			}
 		}
 	}
+	if hasUnavailableRunnerSkip && !hasLiveHappyOrJSONPass {
+		report.Failed++
+		report.MatrixSize++
+		report.Tests = append(report.Tests, LiveDogfoodTestResult{
+			Command: "live-dogfood",
+			Kind:    LiveDogfoodTestHappy,
+			Status:  LiveDogfoodStatusFail,
+			Reason:  reasonNoLiveSignal,
+		})
+	}
 	// Failed-or-empty wins. Skips are non-failures, but quick acceptance still
 	// needs enough counted signal before it can write an acceptance marker.
 	switch {
diff --git a/internal/pipeline/live_dogfood_test.go b/internal/pipeline/live_dogfood_test.go
index ab95e016..dee6b69e 100644
--- a/internal/pipeline/live_dogfood_test.go
+++ b/internal/pipeline/live_dogfood_test.go
@@ -130,6 +130,59 @@ func TestRunLiveDogfoodErrorPathAcceptsExpectedNonZeroExit(t *testing.T) {
 	assert.Equal(t, 2, errorPath.ExitCode)
 }
 
+func TestLiveDogfoodMutatingLeafDetectionTokenizesCommandNames(t *testing.T) {
+	t.Parallel()
+
+	for _, name := range []string{
+		"create-customer",
+		"post-agent-ledger-templates",
+		"request-send-money",
+		"transfer",
+		"set-token",
+	} {
+		assert.True(t, isMutatingLeaf(name), "%s should be treated as mutating", name)
+	}
+	assert.False(t, isMutatingLeaf("get-agent-ledger-templates"))
+	assert.False(t, isMutatingLeaf("list"))
+}
+
+func TestLiveDogfoodCommandMutatesPrefersEndpointMethod(t *testing.T) {
+	t.Parallel()
+
+	assert.False(t, liveDogfoodCommandMutates(liveDogfoodCommand{
+		Path: []string{"search", "query"},
+		Annotations: map[string]string{
+			"pp:method":     "POST",
+			"mcp:read-only": "true",
+		},
+	}))
+	assert.False(t, liveDogfoodCommandMutates(liveDogfoodCommand{
+		Path:        []string{"request-send-money", "list-send-money-approval-requests"},
+		Annotations: map[string]string{"pp:method": "GET"},
+	}))
+	assert.True(t, liveDogfoodCommandMutates(liveDogfoodCommand{
+		Path:        []string{"accounts", "plain-name"},
+		Annotations: map[string]string{"pp:method": "POST"},
+	}))
+}
+
+func TestValidLiveDogfoodJSONOutputAcceptsNDJSON(t *testing.T) {
+	t.Parallel()
+
+	assert.True(t, validLiveDogfoodJSONOutput(`{"event":"start"}`))
+	assert.True(t, validLiveDogfoodJSONOutput("{\"event\":\"start\"}\n{\"event\":\"done\"}\n"))
+	assert.False(t, validLiveDogfoodJSONOutput("{\"event\":\"start\"}\nwarning: skipped\n"))
+	assert.False(t, validLiveDogfoodJSONOutput(""))
+}
+
+func TestLiveDogfoodUnavailableForRunnerDoesNotHideNotFound(t *testing.T) {
+	t.Parallel()
+
+	assert.True(t, liveDogfoodUnavailableForRunner(liveDogfoodRun{stderr: "HTTP 403 permission denied"}))
+	assert.True(t, liveDogfoodUnavailableForRunner(liveDogfoodRun{stderr: "your credentials are valid but lack access"}))
+	assert.False(t, liveDogfoodUnavailableForRunner(liveDogfoodRun{stderr: "HTTP 404 NotFound"}))
+}
+
 func TestRunLiveDogfoodSkipsDestructiveByDefault(t *testing.T) {
 	if runtime.GOOS == "windows" {
 		t.Skip("test uses a shell script as the fake binary; skip on Windows")
@@ -307,6 +360,25 @@ func TestFinalizeLiveDogfoodReportVerdictGate(t *testing.T) {
 			},
 			want: "PASS",
 		},
+		{
+			name:  "full credential-unavailable skips need a live signal",
+			level: "full",
+			results: []LiveDogfoodTestResult{
+				mkResult(LiveDogfoodStatusPass),
+				mkResult(LiveDogfoodStatusPass),
+				{Status: LiveDogfoodStatusSkip, Reason: reasonUnavailableRunnerCredentials},
+			},
+			want: "FAIL",
+		},
+		{
+			name:  "full credential-unavailable skips pass with one real happy path",
+			level: "full",
+			results: []LiveDogfoodTestResult{
+				{Status: LiveDogfoodStatusPass, Kind: LiveDogfoodTestHappy, Args: []string{"widgets", "get"}},
+				{Status: LiveDogfoodStatusSkip, Reason: reasonUnavailableRunnerCredentials},
+			},
+			want: "PASS",
+		},
 		{
 			name:  "full one fail — Failed dominates at full level",
 			level: "full",
@@ -2105,7 +2177,7 @@ func TestRunLiveDogfoodSkipsDryRunInjectionForReadCommand(t *testing.T) {
 		"expected matrix to skip --dry-run injection on non-mutator read commands")
 }
 
-func TestRunLiveDogfoodEmitsErrorPathRealForMutatorWithDryRun(t *testing.T) {
+func TestRunLiveDogfoodSkipsErrorPathRealForMutatorWithDryRun(t *testing.T) {
 	if runtime.GOOS == "windows" {
 		t.Skip("test uses a shell script as the fake binary; skip on Windows")
 	}
@@ -2114,25 +2186,9 @@ func TestRunLiveDogfoodEmitsErrorPathRealForMutatorWithDryRun(t *testing.T) {
 
 	got := findResultByCommandKind(report, "widgets create", LiveDogfoodTestErrorReal)
 	require.NotNil(t, got, "expected error_path_real entry for mutator advertising --dry-run")
-	// API rejection path: fixture exits 1 without --dry-run → Pass.
-	assert.Equal(t, LiveDogfoodStatusPass, got.Status, got.Reason)
-	assert.NotContains(t, got.Args, "--dry-run",
-		"error_path_real must use the original (pre-injection) args to provoke API rejection")
-}
-
-func TestRunLiveDogfoodErrorPathRealFailsWhenAPIAcceptsPlaceholder(t *testing.T) {
-	if runtime.GOOS == "windows" {
-		t.Skip("test uses a shell script as the fake binary; skip on Windows")
-	}
-	dir, binaryName := writeLiveDogfoodDryRunFixture(t)
-	t.Setenv("PRINTING_PRESS_TEST_PERMISSIVE_API", "1")
-	report := runDryRunFixtureMatrix(t, dir, binaryName)
-
-	got := findResultByCommandKind(report, "widgets create", LiveDogfoodTestErrorReal)
-	require.NotNil(t, got)
-	assert.Equal(t, LiveDogfoodStatusFail, got.Status,
-		"over-permissive API quietly accepting placeholder body must surface as Fail")
-	assert.Equal(t, "expected non-zero exit for placeholder body", got.Reason)
+	assert.Equal(t, LiveDogfoodStatusSkip, got.Status, got.Reason)
+	assert.Equal(t, reasonMutatingDryRunOnly, got.Reason)
+	assert.Empty(t, got.Args, "skipped error_path_real must not include executable mutation args")
 }
 
 func TestRunLiveDogfoodSkipsErrorPathRealForReadCommand(t *testing.T) {
diff --git a/testdata/golden/expected/catalog-list/stdout.txt b/testdata/golden/expected/catalog-list/stdout.txt
index 81058c07..d7e798e2 100644
--- a/testdata/golden/expected/catalog-list/stdout.txt
+++ b/testdata/golden/expected/catalog-list/stdout.txt
@@ -23,6 +23,7 @@ other:
   kayak                Flight, hotel, and car rental aggregator. No public API and no maintained community wrappers — reached by scraping embedded JSON from server-rendered pages.
 
 payments:
+  mercury              Business banking API for accounts, transactions, payments, recipients, cards, invoices, treasury, and webhooks
   plaid                Banking API for account linking, transactions, identity verification, and income
   stripe               Payment processing and financial infrastructure API
 
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
index 209f4628..0de66975 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/auth.go
@@ -79,7 +79,7 @@ func newAuthSetTokenCmd(flags *rootFlags) *cobra.Command {
 	return &cobra.Command{
 		Use:   "set-token <token>",
 		Short: "Save an API token to the config file",
-		Example: "  printing-press-rich-pp-cli auth set-token sk_live_abc123",
+		Example: "  printing-press-rich-pp-cli auth set-token YOUR_TOKEN_HERE",
 		Args:  cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cfg, err := config.Load(flags.configPath)
diff --git a/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/internal/types/types.go b/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/internal/types/types.go
index b3b5ed26..69b2517c 100644
--- a/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/internal/types/types.go
+++ b/testdata/golden/expected/generate-golden-api-unicode/cafe-bistro/internal/types/types.go
@@ -9,4 +9,3 @@ type CafeMenu struct {
 	Name string `json:"name"`
 	Specialite string `json:"specialite"`
 }
-
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
index 2b2ae630..3c63c448 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/auth.go
@@ -76,7 +76,7 @@ func newAuthSetTokenCmd(flags *rootFlags) *cobra.Command {
 	return &cobra.Command{
 		Use:   "set-token <token>",
 		Short: "Save an API token to the config file",
-		Example: "  printing-press-golden-pp-cli auth set-token sk_live_abc123",
+		Example: "  printing-press-golden-pp-cli auth set-token YOUR_TOKEN_HERE",
 		Args:  cobra.ExactArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			cfg, err := config.Load(flags.configPath)
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/types/types.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/types/types.go
index 8853a114..0cecb7b0 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/types/types.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/types/types.go
@@ -36,4 +36,3 @@ type UpdateTaskRequest struct {
 	Priority string `json:"priority"`
 	Title string `json:"title"`
 }
-

← cf91eab3 feat(cli): add public parameter names (#648)  ·  back to Cli Printing Press  ·  docs(cli): expand commit-style guidance with type list and b 83d2bb19 →