[object Object]

← back to Cli Printing Press

feat(generator): make generated CLIs agent-native by default (#43)

a8a003daf7ca0a28244de3b563eba8fb7d133f27 · 2026-03-29 09:19:51 -0700 · Matt Van Horn

Templates now generate CLIs optimized for AI agent consumption:
- --compact uses blocklist on single objects (strips description, body,
  comments, attachments) instead of allowlist — works across all API domains
- --agent meta-flag sets --json --compact --no-input --no-color --yes
- Analytics commands (stale, orphans, load) have --limit flag (default 50)
  with total count in output to prevent unbounded context dumps
- Load command resolves assignee names from synced users instead of
  outputting raw Go map debug format
- Search iterates per-table FTS individually to prevent duplicates,
  filters nil/empty entries
- Store template includes IsUUID() and ResolveByName() for name-or-ID
  resolution from synced data (with ambiguity detection)
- Scorecard's AgentNative dimension extended with token efficiency checks
- SKILL.md adds npm/PyPI SDK search to research brief and absorb gate,
  plus search dedup guidance for build phase

Co-authored-by: Matt Van Horn <mvanhorn@MacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit a8a003daf7ca0a28244de3b563eba8fb7d133f27
Author: Matt Van Horn <mvanhorn@users.noreply.github.com>
Date:   Sun Mar 29 09:19:51 2026 -0700

    feat(generator): make generated CLIs agent-native by default (#43)
    
    Templates now generate CLIs optimized for AI agent consumption:
    - --compact uses blocklist on single objects (strips description, body,
      comments, attachments) instead of allowlist — works across all API domains
    - --agent meta-flag sets --json --compact --no-input --no-color --yes
    - Analytics commands (stale, orphans, load) have --limit flag (default 50)
      with total count in output to prevent unbounded context dumps
    - Load command resolves assignee names from synced users instead of
      outputting raw Go map debug format
    - Search iterates per-table FTS individually to prevent duplicates,
      filters nil/empty entries
    - Store template includes IsUUID() and ResolveByName() for name-or-ID
      resolution from synced data (with ambiguity detection)
    - Scorecard's AgentNative dimension extended with token efficiency checks
    - SKILL.md adds npm/PyPI SDK search to research brief and absorb gate,
      plus search dedup guidance for build phase
    
    Co-authored-by: Matt Van Horn <mvanhorn@MacBook-Pro.local>
    Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 internal/generator/templates/helpers.go.tmpl       | 65 +++++++++++------
 internal/generator/templates/root.go.tmpl          | 23 ++++++
 internal/generator/templates/search.go.tmpl        | 63 +++++++++++++++++
 internal/generator/templates/store.go.tmpl         | 61 ++++++++++++++++
 .../generator/templates/workflows/pm_load.go.tmpl  | 82 ++++++++++++++++++++--
 .../templates/workflows/pm_orphans.go.tmpl         | 20 +++++-
 .../generator/templates/workflows/pm_stale.go.tmpl | 29 +++++++-
 internal/pipeline/scorecard.go                     | 21 +++++-
 skills/printing-press/SKILL.md                     | 17 +++--
 9 files changed, 341 insertions(+), 40 deletions(-)

diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 5b7fe427..87720dfc 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -319,44 +319,63 @@ func printOutputWithFlags(w io.Writer, data json.RawMessage, flags *rootFlags) e
 }
 
 // compactFields keeps only the most important fields for agent consumption.
+// For arrays: allowlist of high-gravity fields (no descriptions).
+// For single objects: blocklist that strips known-verbose fields (descriptions, comments, etc.).
 func compactFields(data json.RawMessage) json.RawMessage {
+	// Try array first
+	var items []map[string]any
+	if err := json.Unmarshal(data, &items); err == nil {
+		return compactListFields(items)
+	}
+
+	// Single object — use blocklist
+	var obj map[string]any
+	if err := json.Unmarshal(data, &obj); err == nil {
+		return compactObjectFields(obj)
+	}
+
+	return data
+}
+
+// compactListFields keeps only high-gravity fields for array responses.
+func compactListFields(items []map[string]any) json.RawMessage {
 	keepFields := map[string]bool{
 		"id": true, "name": true, "title": true, "identifier": true,
 		"status": true, "state": true, "type": true, "priority": true,
 		"url": true, "email": true, "key": true,
 		"created_at": true, "updated_at": true, "createdAt": true, "updatedAt": true,
-		"description": true,
 	}
 
-	var items []map[string]any
-	if err := json.Unmarshal(data, &items); err == nil {
-		filtered := make([]map[string]any, 0, len(items))
-		for _, item := range items {
-			compact := map[string]any{}
-			for k, v := range item {
-				if keepFields[k] {
-					compact[k] = v
-				}
-			}
-			filtered = append(filtered, compact)
-		}
-		result, _ := json.Marshal(filtered)
-		return result
-	}
-
-	var obj map[string]any
-	if err := json.Unmarshal(data, &obj); err == nil {
+	filtered := make([]map[string]any, 0, len(items))
+	for _, item := range items {
 		compact := map[string]any{}
-		for k, v := range obj {
+		for k, v := range item {
 			if keepFields[k] {
 				compact[k] = v
 			}
 		}
-		result, _ := json.Marshal(compact)
-		return result
+		filtered = append(filtered, compact)
 	}
+	result, _ := json.Marshal(filtered)
+	return result
+}
 
-	return data
+// compactObjectFields strips known-verbose fields from single-object responses.
+// Uses a blocklist so it works across all API domains (project management, payments, CRM, etc.).
+func compactObjectFields(obj map[string]any) json.RawMessage {
+	stripFields := map[string]bool{
+		"description": true, "body": true, "content": true,
+		"comments": true, "attachments": true, "html": true, "markdown": true,
+	}
+
+	compact := map[string]any{}
+	for k, v := range obj {
+		if !stripFields[k] {
+			compact[k] = v
+		}
+	}
+	result, _ := json.Marshal(compact)
+	return result
 }
 
 // printCSV renders JSON arrays as CSV with header row.
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index d9604cd3..ddf52437 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -27,6 +27,7 @@ type rootFlags struct {
 	noCache      bool
 	noInput      bool
 	yes          bool
+	agent        bool
 	selectFields string
 	configPath   string
 	timeout      time.Duration
@@ -59,6 +60,28 @@ func Execute() error {
 	rootCmd.PersistentFlags().BoolVar(&flags.yes, "yes", false, "Skip confirmation prompts (for agents and scripts)")
 	rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "Disable colored output")
 	rootCmd.PersistentFlags().BoolVar(&humanFriendly, "human-friendly", false, "Enable colored output and rich formatting")
+	rootCmd.PersistentFlags().BoolVar(&flags.agent, "agent", false, "Set all agent-friendly defaults (--json --compact --no-input --no-color --yes)")
+
+	rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
+		if flags.agent {
+			if !cmd.Flags().Changed("json") {
+				flags.asJSON = true
+			}
+			if !cmd.Flags().Changed("compact") {
+				flags.compact = true
+			}
+			if !cmd.Flags().Changed("no-input") {
+				flags.noInput = true
+			}
+			if !cmd.Flags().Changed("yes") {
+				flags.yes = true
+			}
+			if !cmd.Flags().Changed("no-color") {
+				noColor = true
+			}
+		}
+		return nil
+	}
 
 {{- range $name, $resource := .Resources}}
 	rootCmd.AddCommand(new{{camel $name}}Cmd(&flags)) {{/* FuncPrefix matches resource name for top-level */}}
diff --git a/internal/generator/templates/search.go.tmpl b/internal/generator/templates/search.go.tmpl
index e2d9b53a..3d080c32 100644
--- a/internal/generator/templates/search.go.tmpl
+++ b/internal/generator/templates/search.go.tmpl
@@ -8,11 +8,36 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
 	"github.com/spf13/cobra"
 )
 
+// isNilOrEmpty checks whether a JSON object has nil or empty values for
+// common identifier fields (title, name, identifier, id).
+func isNilOrEmpty(raw json.RawMessage) bool {
+	var obj map[string]interface{}
+	if err := json.Unmarshal(raw, &obj); err != nil {
+		return true
+	}
+	for _, key := range []string{"title", "name", "identifier", "id"} {
+		if v, ok := obj[key]; ok {
+			if v == nil {
+				continue
+			}
+			if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
+				return false
+			}
+			// Non-string, non-nil value (e.g. numeric ID) — keep it
+			if _, ok := v.(string); !ok {
+				return false
+			}
+		}
+	}
+	return true
+}
+
 func newSearchCmd(flags *rootFlags) *cobra.Command {
 	var resourceType string
 	var limit int
@@ -54,14 +79,52 @@ Data must be synced first with the sync command. Searches are instant
 			case "{{.Name}}":
 				results, err = db.Search{{pascal .Name}}(query, limit)
 {{- end}}
+{{- end}}
+			case "":
+				// Search all FTS-enabled tables individually to avoid duplicates.
+				// Do NOT use db.Search() here — it queries resources_fts which
+				// overlaps with per-table FTS indexes and causes duplicate results.
+				seen := make(map[string]bool)
+				_ = seen // prevent unused error when no FTS tables exist
+{{- range .Tables}}
+{{- if .FTS5}}
+				{
+					partial, searchErr := db.Search{{pascal .Name}}(query, limit)
+					if searchErr != nil {
+						return fmt.Errorf("search {{.Name}} failed: %w", searchErr)
+					}
+					for _, r := range partial {
+						key := string(r)
+						if !seen[key] {
+							seen[key] = true
+							results = append(results, r)
+						}
+					}
+				}
+{{- end}}
 {{- end}}
 			default:
+				// Unrecognized type — fall back to generic search
 				results, err = db.Search(query, limit)
 			}
 			if err != nil {
 				return fmt.Errorf("search failed: %w", err)
 			}
 
+			// Filter out entries with nil or empty identifier fields.
+			filtered := make([]json.RawMessage, 0, len(results))
+			for _, r := range results {
+				if !isNilOrEmpty(r) {
+					filtered = append(filtered, r)
+				}
+			}
+			results = filtered
+
+			// Enforce limit across aggregated results.
+			if len(results) > limit {
+				results = results[:limit]
+			}
+
 			if len(results) == 0 {
 				fmt.Fprintf(os.Stderr, "No results for %q\n", query)
 				return nil
diff --git a/internal/generator/templates/store.go.tmpl b/internal/generator/templates/store.go.tmpl
index 743824c0..da00087a 100644
--- a/internal/generator/templates/store.go.tmpl
+++ b/internal/generator/templates/store.go.tmpl
@@ -12,12 +12,20 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"time"
 
 	_ "modernc.org/sqlite"
 )
 
+var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
+
+// IsUUID returns true if the input looks like a UUID.
+func IsUUID(s string) bool {
+	return uuidPattern.MatchString(s)
+}
+
 type Store struct {
 	db   *sql.DB
 	path string
@@ -457,3 +465,56 @@ func (s *Store) Status() (map[string]int, error) {
 	}
 	return status, rows.Err()
 }
+
+// ResolveByName resolves a human-readable name to a UUID from synced data.
+// If the input is already a UUID, it is returned as-is.
+// matchFields are JSON field names to search against (e.g., "name", "key", "email").
+func (s *Store) ResolveByName(resourceType string, input string, matchFields ...string) (string, error) {
+	if IsUUID(input) {
+		return input, nil
+	}
+
+	var matches []string
+	for _, field := range matchFields {
+		query := fmt.Sprintf(
+			`SELECT id FROM resources WHERE resource_type = ? AND LOWER(json_extract(data, '$.%s')) = LOWER(?)`,
+			field,
+		)
+		rows, err := s.db.Query(query, resourceType, input)
+		if err != nil {
+			continue
+		}
+		for rows.Next() {
+			var id string
+			if rows.Scan(&id) == nil {
+				// Deduplicate
+				found := false
+				for _, m := range matches {
+					if m == id {
+						found = true
+						break
+					}
+				}
+				if !found {
+					matches = append(matches, id)
+				}
+			}
+		}
+		rows.Close()
+	}
+
+	switch len(matches) {
+	case 0:
+		return "", fmt.Errorf("%s %q not found in local store. Run 'sync' first, or use the UUID directly", resourceType, input)
+	case 1:
+		return matches[0], nil
+	default:
+		hint := matches[0]
+		if len(matches) > 5 {
+			hint = strings.Join(matches[:5], ", ") + "..."
+		} else {
+			hint = strings.Join(matches, ", ")
+		}
+		return "", fmt.Errorf("ambiguous: %q matches %d %s entries (%s). Use the exact UUID instead", input, len(matches), resourceType, hint)
+	}
+}
diff --git a/internal/generator/templates/workflows/pm_load.go.tmpl b/internal/generator/templates/workflows/pm_load.go.tmpl
index bc6e8b2b..18bbfe5f 100644
--- a/internal/generator/templates/workflows/pm_load.go.tmpl
+++ b/internal/generator/templates/workflows/pm_load.go.tmpl
@@ -16,6 +16,7 @@ import (
 
 func newLoadCmd(flags *rootFlags) *cobra.Command {
 	var dbPath string
+	var limit int
 
 	cmd := &cobra.Command{
 		Use:   "load",
@@ -25,6 +26,9 @@ person. Helps identify overloaded team members and unbalanced workload.`,
 		Example: `  # Show workload distribution
   {{.Name}}-pp-cli load
 
+  # Limit results
+  {{.Name}}-pp-cli load --limit 10
+
   # Output as JSON
   {{.Name}}-pp-cli load --json`,
 		RunE: func(cmd *cobra.Command, args []string) error {
@@ -39,6 +43,36 @@ person. Helps identify overloaded team members and unbalanced workload.`,
 			}
 			defer db.Close()
 
+			// Build user name lookup from synced users
+			userNames := map[string]string{}
+			userRows, err := db.Query(`SELECT data FROM resources WHERE resource_type = 'users'`)
+			if err == nil {
+				defer userRows.Close()
+				for userRows.Next() {
+					var userData []byte
+					if userRows.Scan(&userData) != nil {
+						continue
+					}
+					var u map[string]any
+					if json.Unmarshal(userData, &u) != nil {
+						continue
+					}
+					uid := ""
+					if id, ok := u["id"]; ok {
+						uid = fmt.Sprintf("%v", id)
+					}
+					if uid == "" {
+						continue
+					}
+					for _, nk := range []string{"name", "displayName", "display_name"} {
+						if n, ok := u[nk]; ok && n != nil {
+							userNames[uid] = fmt.Sprintf("%v", n)
+							break
+						}
+					}
+				}
+			}
+
 			rows, err := db.Query(`SELECT id, resource_type, data FROM resources`)
 			if err != nil {
 				return fmt.Errorf("querying items: %w", err)
@@ -68,10 +102,33 @@ person. Helps identify overloaded team members and unbalanced workload.`,
 
 				assignee := ""
 				for _, key := range []string{"assigneeId", "assignee_id", "assignee", "ownerId", "owner_id", "owner"} {
-					if v, ok := obj[key]; ok && v != nil {
-						assignee = fmt.Sprintf("%v", v)
-						break
+					v, ok := obj[key]
+					if !ok || v == nil {
+						continue
+					}
+					// Handle nested object: {id: "...", name: "Sheldon"}
+					if m, ok := v.(map[string]any); ok {
+						for _, nameKey := range []string{"name", "displayName", "display_name"} {
+							if n, ok := m[nameKey]; ok && n != nil {
+								assignee = fmt.Sprintf("%v", n)
+								break
+							}
+						}
+						if assignee == "" {
+							if aid, ok := m["id"]; ok {
+								assignee = fmt.Sprintf("%v", aid)
+							}
+						}
+					} else {
+						// Flat string — try to resolve from user lookup
+						raw := fmt.Sprintf("%v", v)
+						if name, ok := userNames[raw]; ok {
+							assignee = name
+						} else {
+							assignee = raw
+						}
 					}
+					break
 				}
 				if assignee == "" || assignee == "<nil>" {
 					assignee = "(unassigned)"
@@ -88,9 +145,14 @@ person. Helps identify overloaded team members and unbalanced workload.`,
 			sort.Slice(entries, func(i, j int) bool { return entries[i].Count > entries[j].Count })
 
 			if flags.asJSON {
+				shown := entries
+				if limit > 0 && len(shown) > limit {
+					shown = shown[:limit]
+				}
 				result := map[string]any{
-					"total":        total,
-					"distribution": entries,
+					"total_count":  total,
+					"showing":      len(shown),
+					"distribution": shown,
 				}
 				enc := json.NewEncoder(cmd.OutOrStdout())
 				enc.SetIndent("", "  ")
@@ -102,10 +164,15 @@ person. Helps identify overloaded team members and unbalanced workload.`,
 				return nil
 			}
 
-			fmt.Fprintf(cmd.OutOrStdout(), "Workload Distribution (%d total items):\n\n", total)
+			shown := entries
+			if limit > 0 && len(shown) > limit {
+				shown = shown[:limit]
+			}
+
+			fmt.Fprintf(cmd.OutOrStdout(), "Workload Distribution (%d total items, showing %d):\n\n", total, len(shown))
 			fmt.Fprintf(cmd.OutOrStdout(), "%-40s %s\n", "ASSIGNEE", "COUNT")
 			fmt.Fprintf(cmd.OutOrStdout(), "%-40s %s\n", "--------", "-----")
-			for _, entry := range entries {
+			for _, entry := range shown {
 				fmt.Fprintf(cmd.OutOrStdout(), "%-40s %d\n", entry.Assignee, entry.Count)
 			}
 			return nil
@@ -113,6 +180,7 @@ person. Helps identify overloaded team members and unbalanced workload.`,
 	}
 
 	cmd.Flags().StringVar(&dbPath, "db", "", "Database path (default: ~/.config/{{.Name}}-pp-cli/store.db)")
+	cmd.Flags().IntVar(&limit, "limit", 50, "Maximum entries to show")
 
 	return cmd
 }
diff --git a/internal/generator/templates/workflows/pm_orphans.go.tmpl b/internal/generator/templates/workflows/pm_orphans.go.tmpl
index 65beb1ea..fefe3b18 100644
--- a/internal/generator/templates/workflows/pm_orphans.go.tmpl
+++ b/internal/generator/templates/workflows/pm_orphans.go.tmpl
@@ -16,6 +16,7 @@ import (
 
 func newOrphansCmd(flags *rootFlags) *cobra.Command {
 	var dbPath string
+	var limit int
 
 	cmd := &cobra.Command{
 		Use:   "orphans",
@@ -113,10 +114,20 @@ such as assignee, project, priority, or labels. Useful for triaging unowned work
 				})
 			}
 
+			totalCount := len(items)
+			if limit > 0 && len(items) > limit {
+				items = items[:limit]
+			}
+
 			if flags.asJSON {
+				result := map[string]any{
+					"total_count": totalCount,
+					"showing":     len(items),
+					"items":       items,
+				}
 				enc := json.NewEncoder(cmd.OutOrStdout())
 				enc.SetIndent("", "  ")
-				return enc.Encode(items)
+				return enc.Encode(result)
 			}
 
 			if len(items) == 0 {
@@ -124,7 +135,11 @@ such as assignee, project, priority, or labels. Useful for triaging unowned work
 				return nil
 			}
 
-			fmt.Fprintf(cmd.OutOrStdout(), "Found %d orphaned items:\n\n", len(items))
+			if totalCount > len(items) {
+				fmt.Fprintf(cmd.OutOrStdout(), "Found %d orphaned items (showing first %d):\n\n", totalCount, len(items))
+			} else {
+				fmt.Fprintf(cmd.OutOrStdout(), "Found %d orphaned items:\n\n", len(items))
+			}
 			fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-15s %-30s %s\n", "ID", "TYPE", "MISSING", "TITLE")
 			fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-15s %-30s %s\n", "----", "----", "-------", "-----")
 			for _, item := range items {
@@ -140,6 +155,7 @@ such as assignee, project, priority, or labels. Useful for triaging unowned work
 	}
 
 	cmd.Flags().StringVar(&dbPath, "db", "", "Database path (default: ~/.config/{{.Name}}-pp-cli/store.db)")
+	cmd.Flags().IntVar(&limit, "limit", 50, "Maximum items to show")
 
 	return cmd
 }
diff --git a/internal/generator/templates/workflows/pm_stale.go.tmpl b/internal/generator/templates/workflows/pm_stale.go.tmpl
index 5a67ac06..c6850e0c 100644
--- a/internal/generator/templates/workflows/pm_stale.go.tmpl
+++ b/internal/generator/templates/workflows/pm_stale.go.tmpl
@@ -18,6 +18,7 @@ func newStaleCmd(flags *rootFlags) *cobra.Command {
 	var days int
 	var team string
 	var dbPath string
+	var limit int
 
 	cmd := &cobra.Command{
 		Use:   "stale",
@@ -62,6 +63,20 @@ the specified number of days. Useful for identifying forgotten or blocked work.`
 
 			query += ` ORDER BY json_extract(data, '$.updatedAt') ASC, json_extract(data, '$.updated_at') ASC`
 
+			// Get total count first
+			countQuery := "SELECT COUNT(*) FROM (" + query + ")"
+			var totalCount int
+			countRow, countErr := db.Query(countQuery, qArgs...)
+			if countErr == nil {
+				defer countRow.Close()
+				if countRow.Next() {
+					countRow.Scan(&totalCount)
+				}
+			}
+
+			query += ` LIMIT ?`
+			qArgs = append(qArgs, limit)
+
 			rows, err := db.Query(query, qArgs...)
 			if err != nil {
 				return fmt.Errorf("querying stale items: %w", err)
@@ -119,9 +134,14 @@ the specified number of days. Useful for identifying forgotten or blocked work.`
 			}
 
 			if flags.asJSON {
+				result := map[string]any{
+					"total_count": totalCount,
+					"showing":     len(items),
+					"items":       items,
+				}
 				enc := json.NewEncoder(cmd.OutOrStdout())
 				enc.SetIndent("", "  ")
-				return enc.Encode(items)
+				return enc.Encode(result)
 			}
 
 			if len(items) == 0 {
@@ -129,7 +149,11 @@ the specified number of days. Useful for identifying forgotten or blocked work.`
 				return nil
 			}
 
-			fmt.Fprintf(cmd.OutOrStdout(), "Found %d stale items (no update in %d+ days):\n\n", len(items), days)
+			if totalCount > len(items) {
+				fmt.Fprintf(cmd.OutOrStdout(), "Found %d stale items (showing first %d, no update in %d+ days):\n\n", totalCount, len(items), days)
+			} else {
+				fmt.Fprintf(cmd.OutOrStdout(), "Found %d stale items (no update in %d+ days):\n\n", len(items), days)
+			}
 			fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-15s %-8s %s\n", "ID", "TYPE", "DAYS", "TITLE")
 			fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-15s %-8s %s\n", "----", "----", "----", "-----")
 			for _, item := range items {
@@ -146,6 +170,7 @@ the specified number of days. Useful for identifying forgotten or blocked work.`
 	cmd.Flags().IntVar(&days, "days", 30, "Number of days without update to consider stale")
 	cmd.Flags().StringVar(&team, "team", "", "Filter by team identifier")
 	cmd.Flags().StringVar(&dbPath, "db", "", "Database path (default: ~/.config/{{.Name}}-pp-cli/store.db)")
+	cmd.Flags().IntVar(&limit, "limit", 50, "Maximum items to show")
 
 	return cmd
 }
diff --git a/internal/pipeline/scorecard.go b/internal/pipeline/scorecard.go
index 039915f6..b2bcea11 100644
--- a/internal/pipeline/scorecard.go
+++ b/internal/pipeline/scorecard.go
@@ -503,8 +503,25 @@ func scoreAgentNative(dir string) int {
 		}
 	}
 	if stdinExamples >= 3 {
-		score += 2
-	} else if stdinExamples >= 1 {
+		score++
+	}
+	// Token efficiency: --agent meta-flag
+	if strings.Contains(rootContent, `"agent"`) && strings.Contains(rootContent, "PersistentPreRun") {
+		score++
+	}
+	// Token efficiency: --compact strips verbose fields on single objects (blocklist approach)
+	if strings.Contains(helpersContent, "compactObjectFields") || strings.Contains(helpersContent, "stripVerboseFields") {
+		score++
+	}
+	// Token efficiency: analytics commands have --limit flag
+	staleContent := readFileContent(filepath.Join(dir, "internal", "cli", "pm_stale.go"))
+	loadContent := readFileContent(filepath.Join(dir, "internal", "cli", "pm_load.go"))
+	if (strings.Contains(staleContent, `"limit"`) || staleContent == "") && (strings.Contains(loadContent, `"limit"`) || loadContent == "") {
+		score++
+	}
+	// Token efficiency: store has ResolveByName for name-or-ID resolution
+	storeContent := readFileContent(filepath.Join(dir, "internal", "store", "store.go"))
+	if strings.Contains(storeContent, "ResolveByName") || strings.Contains(storeContent, "IsUUID") {
 		score++
 	}
 	if score > 10 {
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index fc2720f4..1312b2ba 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -258,6 +258,7 @@ The brief must answer:
 Research checklist:
 - Find the spec or docs source
 - Find the top 1-2 competitors
+- Find official and popular SDK wrappers on npm (`site:npmjs.com`) and PyPI (`site:pypi.org`)
 - Find 2-3 concrete user pain points
 - Identify the highest-gravity entities
 - Pick the top 3-5 commands that matter most
@@ -398,6 +399,8 @@ Run these searches in parallel:
 6. **WebFetch**: Check `github.com/anthropics/claude-plugins-official/tree/main/external_plugins` for official plugin
 7. **WebSearch**: `"<API name>" MCP site:lobehub.com OR site:mcpmarket.com OR site:fastmcp.me`
 8. **WebSearch**: `"<API name>" automation script workflow site:github.com`
+9. **WebSearch**: `"<API name>" SDK wrapper site:npmjs.com`
+10. **WebSearch**: `"<API name>" client library site:pypi.org`
 
 ### Step 1.5b: Catalog every feature into the absorb manifest
 
@@ -416,6 +419,8 @@ For EACH tool found, list EVERY feature/tool/command it provides. Then define ho
 
 Every row = a feature we MUST build. No exceptions. If someone else has it, we have it AND it works offline, with --json, --dry-run, typed exit codes, and SQLite persistence.
 
+SDK wrapper methods should be treated as features to absorb — each public method/function is a feature the CLI should match.
+
 ### Step 1.5c: Identify transcendence features
 
 What compound use cases become possible ONLY when ALL absorbed features live in SQLite together?
@@ -537,6 +542,10 @@ After building each command in Priority 1 and Priority 2, verify these 7 princip
 6. **Composability**: Exit codes are typed (0/2/3/4/5/7), output pipes to `jq` cleanly
 7. **Bounded responses**: `--compact` returns only high-gravity fields, list commands have `--limit`
 
+### Search Dedup Rule
+
+When building cross-entity search commands, use per-table FTS search methods individually. Do NOT combine per-table search with the generic `db.Search()` — this causes duplicate results because the same entities exist in both `resources_fts` and per-table FTS indexes.
+
 ### Priority 1 Review Gate
 
 After completing ALL Priority 1 (absorbed) features, BEFORE starting Priority 2 (transcendence):
@@ -645,15 +654,15 @@ If the next research step does not change those answers, stop and generate.
 
 Do not:
 - write 5 separate mandatory research documents
-- defer all workflows to “future work”
+- defer all workflows to "future work"
 - skip verification because the CLI compiles
 - treat scorecard alone as ship proof
 - discover YAML/URL spec incompatibility late and manually convert specs if the tools can already consume them
 - rerun the whole late-phase gauntlet for cosmetic README polish
-- skip features because “the MCP already handles that” (absorb everything, beat it with offline + agent-native)
-- build only “top 3-5 workflows” when the absorb manifest has 15+ (build them ALL, then transcend)
+- skip features because "the MCP already handles that" (absorb everything, beat it with offline + agent-native)
+- build only "top 3-5 workflows" when the absorb manifest has 15+ (build them ALL, then transcend)
 - generate before the Phase 1.5 Ecosystem Absorb Gate is approved
-- call a CLI “GOAT” without matching every feature the best competitor has
+- call a CLI "GOAT" without matching every feature the best competitor has
 
 ### What counts as success
 

← 334a52af feat(skills): integrate sniff into printing-press skill work  ·  back to Cli Printing Press  ·  feat(skills): add browser-use as primary sniff capture backe 82bc5a37 →