Files
boshu2__agentops/cli/cmd/ao/quickstart.go
T
Bo f534d9f70e fix(cli): stop advertising removed commands in fresh-install output; add cobra-tree guard (#907)
## Summary

A fresh-install UX audit found `ao` emitting commands that do not exist
in the default binary. Verified live by building the CLI and running `ao
quick-start` in a fresh temp git repo with a sandboxed HOME.

- **repo_readiness**: `bd init` → tracker-agnostic `br init --prefix
<prefix> (or bd init --prefix <prefix>)` (product supports both; br
leads for guidance consistency). Dropped the hooks/program/schedule
readiness items — they advertised archived machinery (hookless 3.0,
ADR-0009/0012: `ao init --hooks`, `ao autodev init`, `ao init
--with-schedule`). Replaced literal unexpanded `$product`/`$readme`
actions with real next steps. Tracking presence check now also
recognizes a br `_beads` ledger.
- **CLAUDE.md seed section**: rewrote the "Knowledge Flywheel" block
("session hooks extract learnings", "knowledge compounds automatically",
`ao flywheel status`) to the hookless operating-loop story. Old markers
kept as legacy so re-seeding never duplicates the section.
- **quick-start**: `ao beads ready` (never existed) → `ao beads exec
ready`.
- **root help**: advertised `ao lookup`, which is pruned from the
default spine → `ao verify`.
- **doctor**: br install hint pointed end users at an AGENTS.md their
repos do not have → beads_rust URL + `ao beads dir`.
- **bridge**: deleted dead `FactoryRecommendedCommands` (`ao factory
start --goal`, `ao orchestrate status`).

## Guard

`cli/cmd/ao/advertised_commands{,_test}.go`: extracts every `ao ...`
string from user-facing output (readiness actions, quick-start golden
paths, seeded CLAUDE.md, starter pack, root/quick-start help) and
resolves each against the cobra tree **pruned to the production spine**
— the existing "Stale References" doctor check missed these because the
test binary keeps archived commands registered. Stricter than
`cobra.Find`: group commands reject positional args masquerading as
subcommands (`ao beads ready`), and unknown flags fail (`ao init
--with-schedule`).

## Verification

- `go build` / `go vet` / `golangci-lint` clean; `go test` green (7802
in the four touched packages; full cmd/ao suite green)
- Sandboxed-HOME UAT: every command printed by `ao quick-start` answers
`--help` without "unknown command"
- Pre-push cockpit gate: 31/31 fast/head checks pass (after regen of the
CLI command-surface matrix)

Co-authored-by: boshu <241868352+boshu2@users.noreply.github.com>
2026-07-13 18:23:10 -04:00

622 lines
18 KiB
Go

// practices: [pragmatic-programmer, agile-manifesto]
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/boshu2/agentops/cli/internal/lifecycle"
"github.com/spf13/cobra"
)
var quickstartCmd = &cobra.Command{
Use: "quick-start",
Aliases: []string{"quickstart"},
Short: "Set up AgentOps in your project (5 minutes)",
Long: `Initialize AgentOps in your current project.
This command:
1. Creates .agents/ directory structure
2. Optionally initializes beads (git-native issues)
3. Creates starter knowledge pack
4. Shows one live operating-loop path
5. Ends one command away from a first verdict: it readies the provenance
ledger path, checks a reviewer CLI is reachable (the same check as
'ao doctor'), and prints the exact 'ao verify' command to run next
Examples:
ao quick-start # Full setup with beads
ao quick-start --no-beads # Skip beads initialization
ao quick-start --minimal # Just .agents/ structure`,
RunE: runQuickstart,
}
var (
noBeads bool
minimal bool
)
type quickstartResult struct {
Path string `json:"path"`
DryRun bool `json:"dry_run"`
Minimal bool `json:"minimal"`
NoBeads bool `json:"no_beads"`
Beads string `json:"beads"`
Readiness *lifecycle.ReadinessReport `json:"readiness"`
// FirstVerdict is the final quick-start step: ledger readiness + reviewer
// reachability + the exact next command. Nil on --dry-run (no writes, no
// probes).
FirstVerdict *firstVerdictInfo `json:"first_verdict,omitempty"`
}
func init() {
quickstartCmd.GroupID = "start"
rootCmd.AddCommand(quickstartCmd)
quickstartCmd.Flags().BoolVar(&noBeads, "no-beads", false, "Skip beads initialization")
quickstartCmd.Flags().BoolVar(&minimal, "minimal", false, "Minimal setup (just directories)")
}
// quickstartBeadsStep handles step 3: beads initialization or skip.
func quickstartBeadsStep(cwd string) error {
return quickstartBeadsStepWithApp(cwd, NewApp())
}
func quickstartBeadsStepWithApp(cwd string, app *App) error {
if !noBeads {
fmt.Println("\n━━━ STEP 3: Beads initialization ━━━")
if err := initBeadsWithApp(cwd, app); err != nil {
return fmt.Errorf("tracker initialization failed: %w", err)
}
} else {
fmt.Println("\n━━━ STEP 3: Skipping beads (--no-beads) ━━━")
fmt.Println(" → Issues will be tracked in .agents/tasks.json instead")
createTasksFile(cwd)
}
return nil
}
// quickstartClaudeMdStep handles step 4: create CLAUDE.md if missing.
func quickstartClaudeMdStep(cwd string) {
fmt.Println("\n━━━ STEP 4: Project configuration ━━━")
claudeMdPath := filepath.Join(cwd, "CLAUDE.md")
if _, err := os.Stat(claudeMdPath); os.IsNotExist(err) {
if err := createProjectClaudeMd(cwd); err != nil {
fmt.Printf(" ⚠ Warning: %v\n", err)
} else {
fmt.Println(" ✓ Created CLAUDE.md (project instructions)")
}
} else {
fmt.Println(" ✓ CLAUDE.md already exists")
}
}
func runQuickstart(cmd *cobra.Command, args []string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
jsonMode := GetOutput() == "json"
opts := lifecycle.ReadinessOptions{
Template: detectTemplate(cwd),
DryRun: GetDryRun(),
Minimal: minimal,
NoBeads: noBeads,
}
if GetDryRun() {
return runQuickstartDryRun(cwd, opts)
}
if !jsonMode {
fmt.Println(`
╔══════════════════════════════════════════════════════════════════╗
║ AGENTOPS QUICK START ║
║ Setting up your project for knowledge compounding ║
╚══════════════════════════════════════════════════════════════════╝`)
fmt.Printf("Project: %s\n\n", cwd)
}
if minimal {
return runQuickstartMinimal(cwd, opts, jsonMode)
}
return runQuickstartFull(cwd, opts, jsonMode, AppFromContext(cmd.Context()))
}
func runQuickstartDryRun(cwd string, opts lifecycle.ReadinessOptions) error {
report, err := lifecycle.PlanRepoSeed(cwd, opts)
if err != nil {
return err
}
return outputQuickstartResult(quickstartResult{
Path: cwd,
DryRun: true,
Minimal: minimal,
NoBeads: noBeads,
Beads: beadsReadinessStatus(cwd, noBeads),
Readiness: report,
})
}
func runQuickstartMinimal(cwd string, opts lifecycle.ReadinessOptions, jsonMode bool) error {
if !jsonMode {
fmt.Println("━━━ STEP 1: Creating .agents/ structure ━━━")
}
if err := createQuickstartDirs(cwd); err != nil {
return err
}
report, err := lifecycle.InspectRepoReadiness(cwd, opts)
if err != nil {
return err
}
firstVerdict := prepareFirstVerdict()
if jsonMode {
return outputQuickstartResult(quickstartResult{
Path: cwd,
Minimal: true,
NoBeads: noBeads,
Beads: "skipped-minimal",
Readiness: report,
FirstVerdict: firstVerdict,
})
}
fmt.Println("\n✓ Minimal setup complete!")
printReadinessSummary(report)
showNextSteps(false)
printFirstVerdictStep(firstVerdict)
return nil
}
func runQuickstartFull(cwd string, opts lifecycle.ReadinessOptions, jsonMode bool, app *App) error {
if !jsonMode {
fmt.Println("━━━ STEP 1: Applying core repo seed ━━━")
}
claudePath := filepath.Join(cwd, "CLAUDE.md")
claudeAlreadyExisted, err := ensureProjectClaudeMd(cwd, claudePath)
if err != nil {
return err
}
report, err := lifecycle.ApplyRepoSeed(cwd, opts)
if err != nil {
return err
}
if err := setupGitProtection(cwd, isGitRepository(cwd)); err != nil {
return err
}
if !jsonMode {
fmt.Println(" ✓ Core readiness seed applied")
fmt.Println("\n━━━ STEP 2: Creating starter knowledge pack ━━━")
}
if err := createStarterPack(cwd); err != nil {
if !jsonMode {
fmt.Printf(" ⚠ Warning: %v\n", err)
}
}
beadsStatus := beadsReadinessStatus(cwd, noBeads)
firstVerdict := prepareFirstVerdict()
if jsonMode {
return outputQuickstartResult(quickstartResult{
Path: cwd,
Minimal: false,
NoBeads: noBeads,
Beads: beadsStatus,
Readiness: report,
FirstVerdict: firstVerdict,
})
}
return finalizeQuickstartFull(cwd, claudePath, claudeAlreadyExisted, report, firstVerdict, app)
}
func ensureProjectClaudeMd(cwd, claudePath string) (bool, error) {
if _, err := os.Stat(claudePath); os.IsNotExist(err) {
if err := createProjectClaudeMd(cwd); err != nil {
return false, err
}
return false, nil
}
return true, nil
}
func finalizeQuickstartFull(cwd, claudePath string, claudeAlreadyExisted bool, report *lifecycle.ReadinessReport, firstVerdict *firstVerdictInfo, app *App) error {
if err := quickstartBeadsStepWithApp(cwd, app); err != nil {
return err
}
fmt.Println("\n━━━ STEP 4: Project configuration ━━━")
if claudeAlreadyExisted {
fmt.Println(" ✓ CLAUDE.md already exists")
}
if lifecycle.HasSeedMarker(readFileBestEffort(claudePath)) {
fmt.Println(" ✓ CLAUDE.md has AgentOps instructions")
} else {
fmt.Println(" ⚠ CLAUDE.md missing AgentOps instructions")
}
fmt.Println("\n━━━ SETUP COMPLETE ━━━")
printReadinessSummary(report)
showNextSteps(!noBeads)
printFirstVerdictStep(firstVerdict)
return nil
}
func outputQuickstartResult(result quickstartResult) error {
if GetOutput() == "json" {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(result)
}
if result.DryRun {
fmt.Println("Dry run complete. No files were created.")
}
if result.Readiness != nil {
printReadinessSummary(result.Readiness)
}
return nil
}
func createQuickstartDirs(cwd string) error {
statePaths, err := lifecycle.ResolveReadinessPaths(cwd)
if err != nil {
return err
}
for _, dir := range append(append([]string{}, lifecycle.CoreAgentSubdirs...), lifecycle.CoreStorageSubdirs...) {
path := filepath.Join(statePaths.AgentsDir, dir)
if err := os.MkdirAll(path, 0o700); err != nil {
return fmt.Errorf("failed to create %s: %w", dir, err)
}
if GetOutput() != "json" {
fmt.Printf(" ✓ %s/\n", filepath.ToSlash(filepath.Join(".agents", dir)))
}
}
return nil
}
func printReadinessSummary(report *lifecycle.ReadinessReport) {
fmt.Println("\nAgentOps repo readiness")
for _, layer := range []lifecycle.ReadinessLayer{
lifecycle.LayerCore,
lifecycle.LayerGoals,
lifecycle.LayerInstructions,
lifecycle.LayerTracking,
lifecycle.LayerProduct,
} {
present, total, action := readinessLayerStatus(report, layer)
status := "ready"
if present < total {
status = "next: " + action
}
if total == 0 {
continue
}
fmt.Printf(" %-13s %s (%d/%d)\n", string(layer)+":", status, present, total)
}
fmt.Println("\nNext: follow the live operating-loop path below.")
}
func readinessLayerStatus(report *lifecycle.ReadinessReport, layer lifecycle.ReadinessLayer) (int, int, string) {
var present, total int
action := ""
for _, item := range report.Items {
if item.Layer != layer {
continue
}
total++
if item.Present {
present++
continue
}
if action == "" {
action = item.Action
}
}
if action == "" {
action = "already configured"
}
return present, total, action
}
func beadsReadinessStatus(cwd string, disabled bool) string {
if disabled {
return "disabled"
}
if _, err := os.Stat(filepath.Join(cwd, ".beads")); err == nil {
return "ready"
}
if GetOutput() == "json" {
return "skipped-json"
}
return "pending"
}
func readFileBestEffort(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
return string(data)
}
func createStarterPack(cwd string) error {
// Create a few starter patterns that are universally useful
patterns := map[string]string{
".agents/patterns/context-boundaries.md": `# Pattern: Fresh Context Per Phase
**Tier:** 2 (Pattern)
**Source:** AgentOps multi-epic post-mortem
## Problem
Long sessions accumulate errors. Context pollution causes drift.
## Solution
Use a fresh context for each operating-loop phase and persist the handoff on disk.
## The 40% Rule
| Context % | Success Rate |
|-----------|--------------|
| <40% | 98% |
| 40-60% | ~50% |
| >60% | ~1% |
At 35% context, checkpoint and consider new session.
`,
".agents/patterns/pre-mortem-first.md": `# Pattern: Pre-Mortem Before Implementation
**Tier:** 2 (Pattern)
**Source:** Knowledge Flywheel post-mortem (2026-01-22)
## Problem
Implementation failures are expensive. Debugging takes longer than preventing.
## Solution
Run a pre-mortem on P0/P1 work before implementation:
` + "```bash" + `
ao session bootstrap
# Review findings
# Then implement through the declared skill contract
` + "```" + `
## Evidence
Pre-mortem caught 6 critical issues before implementation:
- API group mismatches
- Path resolution errors
- Migration assumptions
- Schema drift
## When to Skip
- Bug fixes (already understood)
- Single-file changes (<50 lines)
- P2/P3 priority work
`,
".agents/learnings/session-hygiene.md": `# Learning: Session Hygiene
**Date:** Starter Pack
**Tier:** 1 (Learning)
## Key Practices
1. **Always push before saying done**
- Work that isn't pushed didn't happen
- ` + "`git push`" + ` is the final step
2. **Run a post-mortem after epics**
- Captures learnings for the flywheel
- Creates patterns from experience
3. **Check Smart Connections before starting**
- Search for prior art: ` + "`mcp__smart-connections-work__lookup`" + `
- Don't reinvent what exists
4. **Use beads for state**
- ` + "`br ready`" + ` shows unblocked work
- br is git-JSONL-backed (` + "`_beads/issues.jsonl`" + `); run ` + "`br sync --flush-only`" + ` to flush a snapshot
`,
}
statePaths, err := lifecycle.ResolveReadinessPaths(cwd)
if err != nil {
return err
}
for path, content := range patterns {
fullPath := filepath.Join(cwd, path)
if strings.HasPrefix(path, ".agents/") {
fullPath = filepath.Join(statePaths.AgentsDir, strings.TrimPrefix(path, ".agents/"))
}
if err := os.MkdirAll(filepath.Dir(fullPath), 0o700); err != nil {
return err
}
if err := os.WriteFile(fullPath, []byte(content), 0600); err != nil {
return err
}
if GetOutput() != "json" {
fmt.Printf(" ✓ %s\n", path)
}
}
return nil
}
func initBeads(cwd string) error {
return initBeadsWithApp(cwd, NewApp())
}
func initBeadsWithApp(cwd string, app *App) error {
resolution, err := resolveTracker(cwd, os.Environ())
if err != nil {
return err
}
binary, err := app.LookPath(resolution.Tracker)
if err != nil {
return fmt.Errorf("selected tracker %s command not found: %w", resolution.Tracker, err)
}
resolution.Binary = binary
// Check if already initialized
if _, err := os.Stat(resolution.LedgerDir); err == nil {
fmt.Printf(" ✓ %s tracker already initialized\n", resolution.Tracker)
return nil
}
// Determine prefix from directory name
dirName := filepath.Base(cwd)
prefix := strings.ToLower(dirName)
if len(prefix) > 4 {
prefix = prefix[:4]
}
fmt.Printf(" Initializing beads with prefix '%s'...\n", prefix)
// Ask for confirmation
reader := bufio.NewReader(os.Stdin)
fmt.Printf(" Use prefix '%s'? [Y/n]: ", prefix)
response, _ := reader.ReadString('\n')
response = strings.TrimSpace(strings.ToLower(response))
if response == "n" || response == "no" {
fmt.Print(" Enter prefix: ")
prefix, _ = reader.ReadString('\n')
prefix = strings.TrimSpace(prefix)
}
// Run the selected tracker. Availability and execution use the same resolved
// backend so an explicit bd selection can never be preflighted as br.
cmd := app.ExecCommand(resolution.Binary, "init", "--prefix", prefix) // #nosec G204 -- selected br|bd binary.
cmd.Dir = cwd
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%s init failed: %s", resolution.Tracker, string(output))
}
fmt.Printf(" ✓ %s tracker initialized with prefix '%s'\n", resolution.Tracker, prefix)
return nil
}
func createTasksFile(cwd string) {
statePaths, err := lifecycle.ResolveReadinessPaths(cwd)
tasksPath := filepath.Join(cwd, ".agents/tasks.json")
if err == nil {
tasksPath = filepath.Join(statePaths.AgentsDir, "tasks.json")
}
content := `{
"tasks": [],
"note": "Beads-optional mode. Use 'br init' to enable full git-native issues."
}
`
// ag-chvc: surface write failures instead of silently dropping them; only claim
// success after the write actually lands.
if err := os.WriteFile(tasksPath, []byte(content), 0600); err != nil {
fmt.Fprintf(os.Stderr, " ⚠ could not write %s: %v\n", tasksPath, err)
} else if GetOutput() != "json" {
fmt.Println(" ✓ Created .agents/tasks.json (beads-optional mode)")
}
}
func createProjectClaudeMd(cwd string) error {
dirName := filepath.Base(cwd)
content := fmt.Sprintf(`# %s
## Quick Start
`+"```bash"+`
ao quick-start # Repair or inspect the repo seed
ao session bootstrap # Orient the agent in this repository
ao beads exec ready # See unblocked issues when tracking is enabled
`+"```"+`
## Session Protocol
`+"```bash"+`
# Start
ao status # Check AgentOps state
ao beads exec ready # Find available work through the selected tracker
# End
git add .
git commit -m "..."
git push # NEVER stop before pushing
`+"```"+`
## JIT Loading
| Working On | Load |
|------------|------|
| Research | .agents/research/ |
| Implementation | Check existing patterns first |
| Debugging | .agents/learnings/ |
`, dirName) + lifecycle.ClaudeMDSeedSection
return os.WriteFile(filepath.Join(cwd, "CLAUDE.md"), []byte(content), 0600)
}
type quickstartJourneyStep struct {
Title string
Commands []string
}
func quickstartJourney(hasBeads bool) []quickstartJourneyStep {
steps := []quickstartJourneyStep{{
Title: "Orient the agent",
Commands: []string{"ao session bootstrap"},
}}
if hasBeads {
steps = append(steps, quickstartJourneyStep{
Title: "Select tracked work",
Commands: []string{"ao beads tracker", "ao beads exec ready"},
})
} else {
steps = append(steps, quickstartJourneyStep{
Title: "Inspect repository readiness",
Commands: []string{"ao status"},
})
}
steps = append(steps, quickstartJourneyStep{
Title: "Prove the committed change",
Commands: []string{firstVerdictCommand},
})
return steps
}
func showNextSteps(hasBeads bool) {
fmt.Print(`
═══════════════════════════════════════════════════════════════════
LIVE PATH
═══════════════════════════════════════════════════════════════════
`)
for i, step := range quickstartJourney(hasBeads) {
fmt.Printf(" %d. %s:\n", i+1, step.Title)
for _, command := range step.Commands {
// The final verdict is rendered once, with readiness information, by
// printFirstVerdictStep. Keeping it in the typed journey makes the
// terminal contract explicit without printing two competing paths.
if command == firstVerdictCommand {
fmt.Println(" (the final step below)")
continue
}
fmt.Printf(" $ %s\n", command)
}
fmt.Println()
}
fmt.Print(`
Success signal: the run leaves validation evidence and reusable context in .agents/
═══════════════════════════════════════════════════════════════════
"Stateful environment. Stateless agents. One explicit operator lane."
═══════════════════════════════════════════════════════════════════
`)
}