Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 67 additions & 9 deletions cmd/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/InitiatDev/initiat-cli/internal/agent"
initiatconfig "github.com/InitiatDev/initiat-cli/internal/config"
"github.com/InitiatDev/initiat-cli/internal/output"
"github.com/InitiatDev/initiat-cli/internal/prompt"
"github.com/InitiatDev/initiat-cli/internal/scaffold"
"github.com/InitiatDev/initiat-cli/internal/setup"
Expand Down Expand Up @@ -217,10 +218,15 @@ func runAgentIterative(
orchestrator.SetDebugWriter(os.Stdout)
orchestrator.SetPromptInput(prompt.PromptInput)

fmtr := output.NewFormatter(os.Stdout)
fmtr.AgentHeader(execErr.FailedCommand.Phase, execErr.FailedCommand.StepName)

var lastDecision *agent.Decision
var lastApply *agent.ApplyResult

for round := 1; round <= 10; round++ {
fmtr.RoundSeparator(round)

next, done, err := runAgentRound(
ctx,
runner,
Expand All @@ -232,7 +238,7 @@ func runAgentIterative(
setupConfig,
execErr,
lastApply,
round,
fmtr,
)
if err != nil {
return err
Expand Down Expand Up @@ -275,7 +281,7 @@ func runAgentRound(
setupConfig *setup.SetupConfig,
execErr *setup.SetupExecutionError,
lastApply *agent.ApplyResult,
round int,
fmtr *output.Formatter,
) (*agentRoundResult, bool, error) {
decision, applyRes, updatedSetup, changedSomething, err := diagnoseApplyReload(
ctx,
Expand All @@ -285,7 +291,7 @@ func runAgentRound(
setupConfig,
execErr,
lastApply,
round,
fmtr,
)
if err != nil {
return nil, false, err
Expand Down Expand Up @@ -320,7 +326,7 @@ func diagnoseApplyReload(
setupConfig *setup.SetupConfig,
execErr *setup.SetupExecutionError,
lastApply *agent.ApplyResult,
round int,
fmtr *output.Formatter,
) (*agent.Decision, *agent.ApplyResult, *setup.SetupConfig, bool, error) {
snapshotJSON, snapshot, snapErr := agent.BuildProjectSnapshot(wd)
if snapErr != nil {
Expand All @@ -333,17 +339,32 @@ func diagnoseApplyReload(
return nil, nil, nil, false, err
}

fmt.Println()
fmt.Printf("Agent round %d diagnosis:\n", round)
fmt.Println(decision.Explanation)
fmt.Println()
fmtr.Explanation(decision.Explanation)

actionItems := decisionToActionItems(decision)
fmtr.ActionList(actionItems)

beforeSetupFP, _ := fileFingerprint(setupPath)
applyRes, err := orchestrator.ApplyWithResults(ctx, decision)
if err != nil {
return nil, nil, nil, false, err
}

for i, r := range applyRes.Results {
var item output.ActionItem
if i < len(actionItems) {
item = actionItems[i]
} else {
item = output.ActionItem{Summary: r.Summary, Danger: "safe"}
}
detail := ""
if !r.OK && r.Error != "" {
detail = r.Error
}
fmtr.ActionResult(i, item, r.OK, detail)
}
fmt.Fprintln(os.Stdout)

afterSetupFP, _ := fileFingerprint(setupPath)
setupConfig, err = reloadSetupIfChanged(setupPath, setupConfig, beforeSetupFP, afterSetupFP)
if err != nil {
Expand All @@ -356,14 +377,51 @@ func diagnoseApplyReload(
}

if !decisionLikelyChangedSomething(decision, applyRes) {
fmt.Println()
fmt.Println("No changes applied that warrant re-running setup. Continuing agent mode...")
return decision, applyRes, setupConfig, false, nil
}

return decision, applyRes, setupConfig, true, nil
}

func decisionToActionItems(decision *agent.Decision) []output.ActionItem {
items := make([]output.ActionItem, len(decision.Actions))
for i, a := range decision.Actions {
items[i] = output.ActionItem{
Summary: a.Summary,
Danger: string(a.Danger),
Type: string(a.Type),
Detail: actionDetail(a),
}
}
return items
}

func actionDetail(a agent.ProposedAction) string {
switch a.Type {
case agent.ActionRunCommand:
return a.Command
case agent.ActionEditFiles:
paths := make([]string, len(a.Edits))
for i, e := range a.Edits {
paths[i] = e.Path
}
return strings.Join(paths, ", ")
case agent.ActionListFiles:
if a.Path != "" {
return a.Path
}
return "."
case agent.ActionReadFiles:
return strings.Join(a.Paths, ", ")
case agent.ActionAskUser:
return a.Prompt
case agent.ActionStop:
return ""
}
return ""
}

func rerunSetupAndMaybeContinue(
ctx context.Context,
runner *setup.SetupRunner,
Expand Down
18 changes: 9 additions & 9 deletions docs/AGENT_SETUP_IMPROVEMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,28 +57,28 @@ Replace raw `fmt.Println` calls with a structured, visually clear output format

### 2.2 Apply formatter to setup execution

- [ ] Update `Executor.Execute` in `internal/setup/executor.go` to use the formatter instead of raw `fmt.Printf`
- [ ] Show per-command durations on the same line as the step name
- [ ] On failure, show the failing command's stderr indented beneath the step
- [ ] Show skipped phases (phases that never ran due to early failure) at the bottom
- [x] Update `Executor.Execute` in `internal/setup/executor.go` to use the formatter instead of raw `fmt.Printf`
- [x] Show per-command durations on the same line as the step name
- [x] On failure, show the failing command's stderr indented beneath the step
- [x] Show skipped phases (phases that never ran due to early failure) at the bottom

### 2.3 Apply formatter to agent mode

- [ ] Add agent-mode header output:
- [x] Add agent-mode header output:
```
╔══ Agent Mode ═══════════════════════════════════════╗
║ Diagnosing failure in provision → "Run migrations" ║
╚═════════════════════════════════════════════════════╝
```
- [ ] Add round separator: `── Round 1 ──────────────────`
- [ ] Format the diagnosis explanation in a visually distinct block (indented or quoted)
- [ ] Format proposed actions as a numbered list with danger-level badges:
- [x] Add round separator: `── Round 1 ──────────────────`
- [x] Format the diagnosis explanation in a visually distinct block (indented or quoted)
- [x] Format proposed actions as a numbered list with danger-level badges:
```
Proposed actions:
1. [safe] Read db/migrate/ directory listing
2. [caution] Run: rails db:migrate:status
```
- [ ] Show action results inline after execution:
- [x] Show action results inline after execution:
```
1. [safe] Read db/migrate/ directory listing ✓
2. [caution] Run: rails db:migrate:status ✓ (exit 0)
Expand Down
185 changes: 185 additions & 0 deletions internal/output/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,191 @@ func (f *Formatter) PhasesSkipped(names []string) {
}
}

// --- Agent mode ---

// AgentHeader prints the agent-mode banner with the failure context.
//
// Fancy:
//
// ╔══ Agent Mode ═══════════════════════════════════════╗
// ║ Diagnosing failure in provision → "Run migrations" ║
// ╚═════════════════════════════════════════════════════╝
//
// Plain:
//
// === Agent Mode ===
// Diagnosing failure in <phase> -> "<step>"
func (f *Formatter) AgentHeader(phase, step string) {
desc := fmt.Sprintf("Diagnosing failure in %s", phase)
if step != "" {
desc += fmt.Sprintf(" → %q", step)
}

if !f.fancy {
fmt.Fprintf(f.w, "=== Agent Mode ===\n%s\n\n", desc)
return
}

const (
minWidth = 50
sidePadding = 2 // 1 space padding each side
rightPadding = 1 // space before closing ║
)
contentWidth := len(desc) + sidePadding
if contentWidth < minWidth {
contentWidth = minWidth
}

top := "╔══ Agent Mode " + strings.Repeat("═", contentWidth-len("══ Agent Mode ")) + "╗"
padded := "║ " + desc + strings.Repeat(" ", contentWidth-len(desc)-rightPadding) + "║"
bottom := "╚" + strings.Repeat("═", contentWidth) + "╝"

fmt.Fprintln(f.w, f.bold(top))
fmt.Fprintln(f.w, f.bold(padded))
fmt.Fprintln(f.w, f.bold(bottom))
fmt.Fprintln(f.w)
}

// RoundSeparator prints a visual separator between agent rounds.
//
// Fancy: ── Round 1 ──────────────────
// Plain: -- Round 1 --
func (f *Formatter) RoundSeparator(round int) {
label := fmt.Sprintf("Round %d", round)
const roundLineWidth = 40
if f.fancy {
line := "── " + label + " " + strings.Repeat("─", roundLineWidth-len(label))
fmt.Fprintln(f.w, f.dim(line))
} else {
fmt.Fprintf(f.w, "-- %s --\n", label)
}
}

// Explanation prints the agent's diagnosis explanation in a visually
// distinct block (indented/quoted).
//
// Fancy: │ The migration failed because …
// Plain: > The migration failed because …
func (f *Formatter) Explanation(text string) {
if strings.TrimSpace(text) == "" {
return
}
fmt.Fprintln(f.w)
for _, line := range strings.Split(text, "\n") {
if f.fancy {
fmt.Fprintf(f.w, " %s %s\n", f.dim("│"), line)
} else {
fmt.Fprintf(f.w, " > %s\n", line)
}
}
fmt.Fprintln(f.w)
}

// ActionItem describes a single proposed action for display purposes.
type ActionItem struct {
Summary string
Danger string // "safe", "caution", or "dangerous"
Type string // action type like "run_command", "edit_files", etc.
Detail string // command string, file path, prompt, etc.
}

// ActionList prints proposed actions as a numbered list with danger-level badges.
//
// Fancy:
//
// Proposed actions:
// 1. [safe] Read db/migrate/ directory listing
// 2. [caution] Run: rails db:migrate:status
//
// Plain:
//
// Proposed actions:
// 1. [safe] Read db/migrate/ directory listing
// 2. [caution] Run: rails db:migrate:status
func (f *Formatter) ActionList(actions []ActionItem) {
if len(actions) == 0 {
return
}
fmt.Fprintln(f.w, "Proposed actions:")
for i, a := range actions {
badge := f.dangerBadge(a.Danger)
fmt.Fprintf(f.w, " %d. %s %s\n", i+1, badge, a.Summary)
}
fmt.Fprintln(f.w)
}

// ActionResult prints the result of an executed action inline.
//
// Fancy:
//
// 1. [safe] Read db/migrate/ directory listing ✓
// 2. [caution] Run: rails db:migrate:status ✓ (exit 0)
// 3. [caution] Run: rails db:drop ✗ (permission denied)
//
// Plain:
//
// 1. [safe] Read db/migrate/ directory listing [ok]
// 2. [caution] Run: rails db:migrate:status [ok] (exit 0)
// 3. [caution] Run: rails db:drop [FAIL] (permission denied)
func (f *Formatter) ActionResult(index int, action ActionItem, ok bool, detail string) {
badge := f.dangerBadge(action.Danger)
suffix := ""
if detail != "" {
suffix = " (" + detail + ")"
}
if ok {
if f.fancy {
fmt.Fprintf(f.w, " %d. %s %s %s%s\n",
index+1, badge, action.Summary, f.green("✓"), f.dim(suffix))
} else {
fmt.Fprintf(f.w, " %d. %s %s [ok]%s\n",
index+1, badge, action.Summary, suffix)
}
} else {
if f.fancy {
fmt.Fprintf(f.w, " %d. %s %s %s%s\n",
index+1, badge, action.Summary, f.red("✗"), f.red(suffix))
} else {
fmt.Fprintf(f.w, " %d. %s %s [FAIL]%s\n",
index+1, badge, action.Summary, suffix)
}
}
}

const (
dangerBadgeWidth = 11 // len("[dangerous]")
)

func (f *Formatter) dangerBadge(danger string) string {
var raw string
switch danger {
case "safe":
raw = "[safe]"
case "caution":
raw = "[caution]"
case "dangerous":
raw = "[dangerous]"
default:
raw = "[" + danger + "]"
}

if !f.fancy {
return raw
}

padded := raw + strings.Repeat(" ", dangerBadgeWidth-len(raw))
switch danger {
case "safe":
return f.green(padded)
case "caution":
return f.yellow(padded)
case "dangerous":
return f.red(padded)
default:
return padded
}
}

// --- ANSI helpers ---

const (
Expand Down
Loading
Loading