diff --git a/RUBRIC.md b/RUBRIC.md index 76bebbf..68390f8 100644 --- a/RUBRIC.md +++ b/RUBRIC.md @@ -131,9 +131,11 @@ Flag and subcommand removals are breaking changes caught by CI. Agents can depen |---|-----------|------|-----|-----------| | 3A.1 | `SKILL.md` embedded via `go:embed` | Yes | — | `skills/SKILL.md` | | 3A.2 | ` skill` prints embedded skill | Yes | — | `internal/commands/skill.go` | -| 3A.3 | `.claude-plugin/` with plugin.json, hooks, agents | Yes | — | `.claude-plugin/` | +| 3A.3 | `.claude-plugin/` with plugin.json, hooks, commands | Yes | — | `.claude-plugin/` | | 3A.4 | SessionStart hook emits CLI context | Yes | — | `.claude-plugin/hooks/` | | 3A.5 | Skill synced to `basecamp/skills` on release | Yes | — | `scripts/sync-skills.sh` | +| 3A.6 | `setup claude` installs plugin via marketplace | Yes | — | `internal/harness/claude.go`, `internal/commands/setup.go` | +| 3A.7 | Plugin registered in `basecamp/claude-plugins` marketplace | No | — | `basecamp/claude-plugins` marketplace.json | ### 3B. Pagination @@ -219,9 +221,9 @@ For the **TUI tool profile**, score only the applicable tiers (1D, 4A, 4B, 4D) a |------|-------|-----| | T1: Agent Contract | /26 | 26 | | T2: Reliability | /16 | 16 | -| T3: Agent Integration | /11 | 11 | +| T3: Agent Integration | /13 | 13 | | T4: Distribution | /29 | 29 | -| **Total** | **/82** | **82** | +| **Total** | **/84** | **84** | ### Detailed Results @@ -274,6 +276,8 @@ For the **TUI tool profile**, score only the applicable tiers (1D, 4A, 4B, 4D) a | 3A.3 | `.claude-plugin/` | | | | | 3A.4 | SessionStart hook | | | | | 3A.5 | Skill synced on release | | | | +| 3A.6 | `setup claude` | | | | +| 3A.7 | Marketplace registration | | | | | 3B.1 | `--limit N` | | | | | 3B.2 | `--all` | | | | | 3B.3 | Truncation notice | | | | diff --git a/prompts/close-gap.md b/prompts/close-gap.md index 7d54795..c2f0800 100644 --- a/prompts/close-gap.md +++ b/prompts/close-gap.md @@ -48,3 +48,16 @@ Replace file-only credential storage with `credstore.NewStore()`. Set ServiceNam ### Adding surface stability (2A.2-3) Use the `surface` package to generate snapshots. Commit the baseline. Add the `surface-compat` GitHub Action to CI. + +### Adding setup claude (3A.6) +Create `internal/harness/claude.go` with `ClaudeMarketplaceSource` and `ClaudePluginName` +constants, plus `DetectClaude`, `FindClaudeBinary`, `IsPluginNeeded`, and `CheckClaudePlugin` +functions. Add a `setup claude` subcommand that runs marketplace add (best-effort) then +plugin install, with verify-after-install. Wire into the main setup wizard and add +breadcrumb suggestions via `harness.IsPluginNeeded()`. +Reference: github.com/basecamp/basecamp-cli/internal/harness/claude.go and wizard.go. + +### Marketplace registration (3A.7) +Manual, external follow-up. Add a plugin entry to `basecamp/claude-plugins` +marketplace.json with source pointing at `basecamp/-cli`. This is a one-time +step in the marketplace repo, not automatable from within the CLI repo. diff --git a/prompts/seed-cli.md b/prompts/seed-cli.md index 0478f83..32d262c 100644 --- a/prompts/seed-cli.md +++ b/prompts/seed-cli.md @@ -18,6 +18,7 @@ You are creating a new Go CLI for a 37signals product using the seed templates. │ ├── auth/ │ ├── commands/ │ ├── config/ + │ ├── harness/ │ └── output/ ├── e2e/ ├── skills/ @@ -83,9 +84,11 @@ You are creating a new Go CLI for a 37signals product using the seed templates. - `seed/internal/commands/doctor.go.tmpl` → `internal/commands/doctor.go` - `seed/internal/commands/setup.go.tmpl` → `internal/commands/setup.go` - `seed/internal/commands/skill.go.tmpl` → `internal/commands/skill.go` + - `seed/internal/harness/claude.go.tmpl` → `internal/harness/claude.go` (fill in app name) **Skills & plugin:** - `seed/.claude-plugin/` → `.claude-plugin/` (customize) + - `mkdir -p .claude-plugin/skills && ln -s ../../skills/ .claude-plugin/skills/` (create skills symlink) - `seed/skills/app/SKILL.md.tmpl` → `skills//SKILL.md` (customize) - `seed/skills/embed.go.tmpl` → `skills/embed.go` @@ -157,6 +160,12 @@ After the repo is pushed to GitHub: pip install pre-commit && pre-commit install --install-hooks ``` +5. **Claude plugin marketplace** — register in `basecamp/claude-plugins`: + - Clone `basecamp/claude-plugins` + - Add entry to `.claude-plugin/marketplace.json` plugins array: + `{"name": "", "description": "...", "source": {"source": "github", "repo": "basecamp/-cli"}, "category": "productivity"}` + - PR and merge + ## Auth Model Configuration ### OAuth + PKCE diff --git a/seed/.claude-plugin/agents/context-linker.md b/seed/.claude-plugin/agents/context-linker.md deleted file mode 100644 index e4fb729..0000000 --- a/seed/.claude-plugin/agents/context-linker.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -name: context-linker -description: | - Automatically link code changes to {{.Name}} items. - Use when: committing code, creating PRs, resolving issues. - Detects item IDs from branch names, commit messages, and PR descriptions. ---- - -# Context Linker Agent - -Connect code changes to {{.Name}} items. - -## Detection Patterns - -Look for references in: - -1. **Branch names**: `feature/todo-12345-description`, `fix/12345-bug` -2. **Commit messages**: `[#12345] Fix bug`, `Fixes #12345` -3. **PR descriptions**: `Closes #12345`, `Related: ` - -## Workflow: On Commit - -1. Extract item ID from branch name: - ```bash - BRANCH=$(git branch --show-current) - ITEM_ID=$(echo "$BRANCH" | grep -oE '[0-9]+' | head -1) - ``` - -2. If found, offer to link: - ```bash - COMMIT=$(git rev-parse --short HEAD) - MSG=$(git log -1 --format=%s) - # Add comment or update linked item - ``` - -## Workflow: On PR Creation - -1. Check branch name and PR description for item references -2. For each referenced item, add PR link -3. Offer to update item status when PR is merged diff --git a/seed/.claude-plugin/agents/navigator.md b/seed/.claude-plugin/agents/navigator.md deleted file mode 100644 index e69fae7..0000000 --- a/seed/.claude-plugin/agents/navigator.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: {{.Name}}-navigator -description: | - Cross-resource search and navigation for {{.Name}}. - Use when the user needs to find items, discover structure, - or navigate the workspace. -tools: - - Bash - - Read -model: sonnet ---- - -# {{.Name}} Navigator Agent - -You help users find and navigate {{.Name}} resources. - -## Capabilities - -1. **Search** — Find resources by content or attributes -2. **Discover structure** — List available resources and their relationships -3. **Filter and sort** — By status, assignee, date, type -4. **Navigate context** — Drill down into specific items - -## Available Commands - -```bash -# Discovery -{{.Name}} list -{{.Name}} show - -# Search -{{.Name}} search "query" - -# Filtered listing -{{.Name}} list --status active --limit 20 -``` - -## Search Strategy - -1. Use full-text search for content queries -2. Use list commands with filters for browsing -3. Narrow by known context (project, account, etc.) - -## Output - -- Show item ID for follow-up actions -- Include parent context for clarity -- Offer breadcrumb actions (view, update, delete) diff --git a/seed/.claude-plugin/commands/doctor.md b/seed/.claude-plugin/commands/doctor.md new file mode 100644 index 0000000..2437063 --- /dev/null +++ b/seed/.claude-plugin/commands/doctor.md @@ -0,0 +1,26 @@ +--- +name: {{.Name}}-doctor +description: Check {{.Name}} plugin health — CLI, auth, API connectivity, project context. +invocable: true +--- + +# /{{.Name}}-doctor + +Run the {{.Name}} CLI health check and report results. + +```bash +{{.Name}} doctor --json +``` + +Interpret the output: +- **pass**: Working correctly +- **warn**: Non-critical issue (e.g., shell completion not installed) +- **skip**: Check not run (e.g., unauthenticated or not applicable) +- **fail**: Broken — needs attention + +For any failures, follow the `hint` field in the check output. Common fixes: +- Authentication failed → `{{.Name}} auth login` +- API unreachable → check network / VPN +- Plugin not installed → `{{.Name}} setup claude` + +Report results concisely: list failures and warnings with their hints. If everything passes, say so. diff --git a/seed/.claude-plugin/hooks/hooks.json b/seed/.claude-plugin/hooks/hooks.json new file mode 100644 index 0000000..a5af7c4 --- /dev/null +++ b/seed/.claude-plugin/hooks/hooks.json @@ -0,0 +1,27 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh", + "timeout": 5 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/post-commit-check.sh", + "timeout": 5 + } + ] + } + ] + } +} diff --git a/seed/.claude-plugin/hooks/post-commit-check.sh b/seed/.claude-plugin/hooks/post-commit-check.sh new file mode 100755 index 0000000..4b26836 --- /dev/null +++ b/seed/.claude-plugin/hooks/post-commit-check.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# post-commit-check.sh - Check for {{.Name}} item references after git commits +# +# This hook runs after Bash tool use and checks if a git commit was made +# that references a {{.Name}} item ({{.Upper}}-12345, item-12345, etc.) + +set -euo pipefail + +# Require jq for JSON parsing +if ! command -v jq &>/dev/null; then + exit 0 +fi + +# Read tool input from stdin (JSON with tool_name, tool_input, tool_output) +input=$(cat) + +# Extract tool input (the bash command that was run) +tool_input=$(echo "$input" | jq -r '.tool_input.command // empty' 2>/dev/null) + +# Only process git commit commands +if [[ ! "$tool_input" =~ ^git\ commit ]]; then + exit 0 +fi + +# Check if commit succeeded by looking for output patterns +tool_output=$(echo "$input" | jq -r '.tool_output // empty' 2>/dev/null) + +# Skip if commit failed — detect error indicators before checking for success. +# Only match lines that look like git/hook errors, not commit subject lines +# (e.g. "[branch abc1234] Fix failed login" should not trigger this guard). +# We strip the "[branch hash] subject" success line before scanning for errors. +filtered_output=$(echo "$tool_output" | grep -v '^\[.*[a-f0-9]\{7,\}\]' || true) +if echo "$filtered_output" | grep -qiE '(^|[[:space:]])(error|fatal|aborted|rejected)[[:space:]:]|hook[[:space:]].*[[:space:]]failed|pre-commit[[:space:]].*[[:space:]]failed|^error:'; then + exit 0 +fi + +# Verify commit actually succeeded - look for commit hash pattern or "create mode" +if [[ ! "$tool_output" =~ \[.*[a-f0-9]{7,}\] ]] && [[ ! "$tool_output" =~ "create mode" ]]; then + exit 0 +fi + +# Look for item references in the commit message or branch name +branch=$(git branch --show-current 2>/dev/null || true) +last_commit_msg=$(git log -1 --format=%s 2>/dev/null || true) + +# Patterns: {{.Upper}}-12345, item-12345, {{.Name}}-12345 +todo_patterns='{{.Upper}}-[0-9]+|item-[0-9]+|{{.Name}}-[0-9]+' + +found_in_branch=$(echo "$branch" | grep -oEi "$todo_patterns" | head -1 || true) +found_in_msg=$(echo "$last_commit_msg" | grep -oEi "$todo_patterns" | head -1 || true) + +if [[ -n "$found_in_branch" ]] || [[ -n "$found_in_msg" ]]; then + ref="${found_in_msg:-$found_in_branch}" + # Extract just the number + item_id=$(echo "$ref" | grep -oE '[0-9]+') + + short_sha=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") + comment="Commit ${short_sha}: ${last_commit_msg}" + escaped_comment=$(printf '%q' "$comment") + + cat << EOF + +Detected {{.Name}} item reference: $ref + +To link this commit to {{.Name}}: + {{.Name}} comment ${escaped_comment} --on $item_id + +Or complete the item: + {{.Name}} done $item_id + +EOF +fi diff --git a/seed/.claude-plugin/hooks/session-start.sh b/seed/.claude-plugin/hooks/session-start.sh index 4729364..70bef68 100755 --- a/seed/.claude-plugin/hooks/session-start.sh +++ b/seed/.claude-plugin/hooks/session-start.sh @@ -1,76 +1,50 @@ #!/usr/bin/env bash -# session-start.sh — Load CLI context at session start for Claude Code. +# session-start.sh - {{.Name}} plugin liveness check # -# Emits config, auth status, and active profile so the agent knows -# what environment it's operating in. -# -# TODO: Replace CLI_NAME, config paths, and commands for your CLI. +# Lightweight: one subprocess call. Context priming happens on first +# use via the /{{.Name}} skill, not here. set -euo pipefail -CLI_NAME="${CLI_NAME:-mycli}" +CLI="{{.Name}}" -# Require jq for JSON parsing -if ! command -v jq &>/dev/null; then +if ! command -v "$CLI" &>/dev/null; then + cat << EOF + +$CLI plugin active — CLI not found on PATH. +Install: https://github.com/basecamp/${CLI}-cli#installation + +EOF exit 0 fi -# Find the CLI binary — prefer PATH, fall back to plugin's bin directory -if command -v "$CLI_NAME" &>/dev/null; then - CLI_BIN="$CLI_NAME" -else - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - CLI_BIN="${SCRIPT_DIR}/../../bin/${CLI_NAME}" - if [[ ! -x "$CLI_BIN" ]]; then - cat << EOF +if ! command -v jq &>/dev/null; then + cat << EOF -${CLI_NAME} plugin: CLI not found. -Install: https://github.com/basecamp/${CLI_NAME}-cli#installation +$CLI plugin active. EOF - exit 0 - fi -fi - -# Get CLI version -cli_version=$("$CLI_BIN" --version 2>/dev/null | awk '{print $NF}' || true) - -# Check if we have configuration -config_output=$("$CLI_BIN" config show --json 2>/dev/null || echo '{}') -has_config=$(echo "$config_output" | jq -r '.data // empty' 2>/dev/null) - -if [[ -z "$has_config" ]] || [[ "$has_config" == "{}" ]]; then exit 0 fi -# Build context message -context="${CLI_NAME} context loaded:" +auth_json=$("$CLI" auth status --json 2>/dev/null || echo '{}') -if [[ -n "$cli_version" ]]; then - context+="\n CLI: v${cli_version}" +is_auth=false +if parsed_auth=$(echo "$auth_json" | jq -er '.data.authenticated' 2>/dev/null); then + is_auth="$parsed_auth" fi -# Show active profile if using named profiles -active_profile=$("$CLI_BIN" profile show --json 2>/dev/null | jq -r '.data.name // empty' 2>/dev/null || true) -if [[ -n "$active_profile" ]]; then - context+="\n Profile: $active_profile" -fi - -# Check if authenticated -auth_status=$("$CLI_BIN" auth status --json 2>/dev/null || echo '{}') -is_auth=$(echo "$auth_status" | jq -r '.data.authenticated // false') - -if [[ "$is_auth" != "true" ]]; then - context+="\n Auth: Not authenticated (run: ${CLI_NAME} auth login)" -fi - -cat << EOF +if [[ "$is_auth" == "true" ]]; then + cat << EOF -$(echo -e "$context") - -Use \`${CLI_NAME}\` commands to interact with the API: - ${CLI_NAME} auth login # Authenticate - ${CLI_NAME} auth status # Check auth status - ${CLI_NAME} doctor # Diagnose configuration +$CLI plugin active. EOF +else + cat << EOF + +$CLI plugin active — not authenticated. +Run: $CLI auth login + +EOF +fi diff --git a/seed/.claude-plugin/plugin.json b/seed/.claude-plugin/plugin.json index dbfadb5..c1052a4 100644 --- a/seed/.claude-plugin/plugin.json +++ b/seed/.claude-plugin/plugin.json @@ -6,11 +6,7 @@ "name": "37signals", "email": "support@37signals.com" }, - "hooks": { - "SessionStart": ["hooks/session-start.sh"] - }, - "agents": [ - "agents/navigator.md", - "agents/context-linker.md" - ] + "homepage": "https://github.com/basecamp/{{.Name}}-cli", + "repository": "https://github.com/basecamp/{{.Name}}-cli", + "license": "MIT" } diff --git a/seed/AGENTS.md.tmpl b/seed/AGENTS.md.tmpl index 64a3577..bb23e9e 100644 --- a/seed/AGENTS.md.tmpl +++ b/seed/AGENTS.md.tmpl @@ -12,6 +12,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for build setup, testing, and PR workflow ├── internal/ │ ├── auth/ # Authentication │ ├── commands/ # Command implementations +│ ├── harness/ # Claude Code plugin detection and install │ ├── config/ # Configuration management │ ├── output/ # Output formatting (wraps github.com/basecamp/cli/output) │ └── sdk/ # API SDK wrapper diff --git a/seed/internal/commands/doctor.go.tmpl b/seed/internal/commands/doctor.go.tmpl index 74e0d22..577f7b4 100644 --- a/seed/internal/commands/doctor.go.tmpl +++ b/seed/internal/commands/doctor.go.tmpl @@ -16,7 +16,8 @@ package commands // 6. Account Access — authenticated user can list resources // 7. Cache — cache directory exists, size reasonable // 8. Shell Completion — completion script installed for current shell -// 9. Claude Plugin — .claude-plugin/ installed, hooks present +// 9. Claude Plugin — harness.CheckClaudePlugin() verifies marketplace install +// Hint on failure: "Run: {{.Name}} setup claude" // // Implementation pattern: // diff --git a/seed/internal/commands/setup.go.tmpl b/seed/internal/commands/setup.go.tmpl index 0757375..ecc2bf7 100644 --- a/seed/internal/commands/setup.go.tmpl +++ b/seed/internal/commands/setup.go.tmpl @@ -48,3 +48,70 @@ package commands // AccountName string `json:"account_name,omitempty"` // ConfigScope string `json:"config_scope,omitempty"` // "global", "local" // } + +// Subcommand: {{.Name}} setup claude +// +// Dedicated Claude Code plugin installation. Called by the main wizard +// as step 6, or run standalone by the user. +// +// Pattern: +// +// cmd.AddCommand(newSetupClaudeCmd()) +// +// func newSetupClaudeCmd() *cobra.Command { +// return &cobra.Command{ +// Use: "claude", +// Short: "Install the {{.Name}} plugin for Claude Code", +// RunE: runSetupClaude, +// } +// } +// +// func runSetupClaude(cmd *cobra.Command, args []string) error { +// detected := harness.DetectClaude() || harness.FindClaudeBinary() != "" +// +// // Attempt install if Claude is present and plugin is missing +// if detected { +// if harness.CheckClaudePlugin().Status != "pass" { +// claudePath := harness.FindClaudeBinary() +// if claudePath != "" { +// ctx := cmd.Context() +// // Step 1: Register marketplace (best-effort) +// mktCmd := exec.CommandContext(ctx, claudePath, +// "plugin", "marketplace", "add", harness.ClaudeMarketplaceSource) +// mktCmd.Stderr = cmd.ErrOrStderr() +// _ = mktCmd.Run() +// // Step 2: Install plugin +// installCmd := exec.CommandContext(ctx, claudePath, +// "plugin", "install", harness.ClaudePluginName) +// installCmd.Stdout = cmd.OutOrStdout() +// installCmd.Stderr = cmd.ErrOrStderr() +// _ = installCmd.Run() +// } +// // If claudePath == "", show manual install hint +// } +// } +// +// // Always return a structured envelope +// installed := detected && harness.CheckClaudePlugin().Status == "pass" +// summary := "Claude Code plugin installed" +// if !detected { +// summary = "Claude Code not detected" +// } else if !installed { +// summary = "Claude Code plugin not installed" +// } +// return app.OK(map[string]any{ +// "plugin_installed": installed, +// "claude_detected": detected, +// }, output.WithSummary(summary)) +// } +// +// Other commands suggest setup claude via breadcrumbs when plugin is missing: +// +// if harness.IsPluginNeeded() { +// breadcrumbs = append(breadcrumbs, Breadcrumb{ +// Action: "setup_claude", Cmd: "{{.Name}} setup claude", +// Description: "Connect Claude to {{.Name}}", +// }) +// } +// +// Reference: github.com/basecamp/basecamp-cli/internal/commands/wizard.go diff --git a/seed/internal/harness/claude.go.tmpl b/seed/internal/harness/claude.go.tmpl new file mode 100644 index 0000000..4b4f008 --- /dev/null +++ b/seed/internal/harness/claude.go.tmpl @@ -0,0 +1,40 @@ +package harness + +// Claude Code plugin detection and marketplace install utilities. +// +// Constants: +// +// const ClaudeMarketplaceSource = "basecamp/claude-plugins" +// const ClaudePluginName = "{{.Name}}" +// +// Detection: +// +// func DetectClaude() bool +// Check if ~/.claude/ directory exists. +// +// func FindClaudeBinary() string +// exec.LookPath("claude"), fallback ~/.local/bin/claude. +// Returns "" if not found. +// +// func IsPluginNeeded() bool +// DetectClaude() && CheckClaudePlugin().Status != "pass" +// +// Plugin check: +// +// func CheckClaudePlugin() *StatusCheck +// Read ~/.claude/plugins/installed_plugins.json. +// Handle multiple JSON shapes resilient to schema changes: +// 1. Try as []map[string]any — match name/package/id fields +// 2. Try as map[string]any — match keys +// 3. Fallback: raw string search +// Match on "{{.Name}}" or "{{.Name}}@{{.Name}}". +// Returns StatusCheck{Name, Status, Message, Hint}. +// +// type StatusCheck struct { +// Name string `json:"name"` +// Status string `json:"status"` // "pass", "fail", "warn" +// Message string `json:"message"` +// Hint string `json:"hint,omitempty"` +// } +// +// Reference: github.com/basecamp/basecamp-cli/internal/harness/claude.go diff --git a/skills/rubric-audit/SKILL.md b/skills/rubric-audit/SKILL.md index abd9d06..9ee3da0 100644 --- a/skills/rubric-audit/SKILL.md +++ b/skills/rubric-audit/SKILL.md @@ -69,6 +69,16 @@ Run this skill in the root of a Go CLI repository to produce a gap report. - [ ] Check for --limit, --all flags on list commands - [ ] Check for --verbose, APP_DEBUG +#### 3A.6: setup claude +- [ ] Check for `internal/harness/` directory with Claude detection code +- [ ] Run ` setup claude --help` — subcommand exists +- [ ] Grep for `ClaudeMarketplaceSource` or `basecamp/claude-plugins` in harness code + +#### 3A.7: Marketplace registration (manual, external) +- [ ] This criterion requires checking the external `basecamp/claude-plugins` repo +- [ ] If not locally available, flag as "cannot verify — check basecamp/claude-plugins manually" +- [ ] If available: verify `.claude-plugin/marketplace.json` contains an entry with `"name": ""` + ### 5. Check Tier 4: Distribution & Ecosystem - [ ] Check for .goreleaser.yaml @@ -88,9 +98,9 @@ Produce a scorecard: |------|-------|-----| | T1: Agent Contract | X/26 | 26 | | T2: Reliability | X/16 | 16 | -| T3: Agent Integration | X/11 | 11 | +| T3: Agent Integration | X/13 | 13 | | T4: Distribution | X/29 | 29 | -| **Total** | **X/82** | **82** | +| **Total** | **X/84** | **84** | ### Critical Gaps 1. [Most impactful gap]