From 6922aa3039cebc4cb0193ae77bb08fdf13e5b9bb Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 25 Sep 2026 22:19:25 -0400 Subject: [PATCH 1/5] telemetry: --help on the group and on every verb exits 0 `codeaf telemetry --help` took the flag for a sixth verb and left with 1, and each verb's own --help answered Go's `flag: help requested` and 1, while the manual says asking for help is never a failure. The verbs now parse through the binary's shared help seam and the group answers the gesture with its usage line. Co-Authored-By: Claude Opus 5.5 (1M context) --- cmd/codeaf/telemetry.go | 20 ++++++++++++-------- cmd/codeaf/usage_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/cmd/codeaf/telemetry.go b/cmd/codeaf/telemetry.go index 8b76b04774..7a24618e28 100644 --- a/cmd/codeaf/telemetry.go +++ b/cmd/codeaf/telemetry.go @@ -28,6 +28,10 @@ func runTelemetry(args []string) error { return runTelemetryStatus(nil) } switch args[0] { + case "-h", "-help", "--help": + // ASKING IS NEVER A FAILURE. This door took the flag for a sixth + // verb and left with 1, the one door in the binary that did. + return commandHelp("telemetry") case "status": return runTelemetryStatus(args[1:]) case "info": @@ -42,11 +46,11 @@ func runTelemetry(args []string) error { } // telemetryFlags holds the one flag every verb accepts so a --help reader and -// the tests share one parser. +// the tests share one parser. It is the binary's own seam, named for the whole +// line a person typed, so `codeaf telemetry status --help` prints the usage and +// leaves with 0 like every other verb instead of `flag: help requested` and 1. func telemetryFlags(name string) *flag.FlagSet { - flags := flag.NewFlagSet(name, flag.ContinueOnError) - flags.SetOutput(os.Stderr) - return flags + return commandFlags("telemetry " + name) } // runTelemetryStatus prints the pipe's whole answer: on or off, why it is off, @@ -55,7 +59,7 @@ func telemetryFlags(name string) *flag.FlagSet { // recognise, far too little to be a person. func runTelemetryStatus(args []string) error { flags := telemetryFlags("status") - if err := flags.Parse(args); err != nil { + if err := parseCommandFlags(flags, args); err != nil { return err } // The config answer goes through the package's single door so the row @@ -110,7 +114,7 @@ func telemetryInstallPrefix() string { // here, each under a line naming where it goes or why it does not. func runTelemetryInfo(args []string) error { flags := telemetryFlags("info") - if err := flags.Parse(args); err != nil { + if err := parseCommandFlags(flags, args); err != nil { return err } profileDir := config.ProfileDir() @@ -127,7 +131,7 @@ func runTelemetryInfo(args []string) error { // it, not piping it; a pipe reads indented JSON just as well. func runTelemetryShow(args []string) error { flags := telemetryFlags("show") - if err := flags.Parse(args); err != nil { + if err := parseCommandFlags(flags, args); err != nil { return err } profileDir := config.ProfileDir() @@ -371,7 +375,7 @@ func poolRowsWaiting(poolDir string) []json.RawMessage { // nobody could audit. func runTelemetrySet(word string, args []string) error { flags := telemetryFlags(word) - if err := flags.Parse(args); err != nil { + if err := parseCommandFlags(flags, args); err != nil { return err } profileDir := config.ProfileDir() diff --git a/cmd/codeaf/usage_test.go b/cmd/codeaf/usage_test.go index f61a6f7919..2fb1f8a9a8 100644 --- a/cmd/codeaf/usage_test.go +++ b/cmd/codeaf/usage_test.go @@ -3,6 +3,8 @@ package main import ( "bytes" "errors" + "os" + "path/filepath" "strconv" "strings" "testing" @@ -67,6 +69,15 @@ func TestAskingForHelpIsNotAFailure(t *testing.T) { {"plan run", func(args []string) error { return runGraph("plan run", args) }}, {"services", runServices}, {"models", runModels}, + // The telemetry group and each of its verbs. The group took `--help` + // for a sixth verb and every verb answered `flag: help requested`, + // all with 1, while the manual says help on any verb exits 0. + {"telemetry", runTelemetry}, + {"telemetry status", telemetryVerb("status")}, + {"telemetry info", telemetryVerb("info")}, + {"telemetry show", telemetryVerb("show")}, + {"telemetry on", telemetryVerb("on")}, + {"telemetry off", telemetryVerb("off")}, // The two old top-level spellings. They still open, and asking one for // help says NOTHING on stderr: `--help` runs nothing, so there is no run // for the rename notice to be about, and a Makefile that probes the @@ -112,6 +123,29 @@ func TestAskingForHelpIsNotAFailure(t *testing.T) { } } +// telemetryVerb is `codeaf telemetry ` as the dispatch reaches it. +func telemetryVerb(verb string) func([]string) error { + return func(args []string) error { return runTelemetry(append([]string{verb}, args...)) } +} + +// ASKING `off` FOR HELP TURNS NOTHING OFF. `--help` runs nothing, so a person +// reading what `codeaf telemetry off` does has not yet chosen to do it. +func TestAskingTelemetryOffForHelpChangesNothing(t *testing.T) { + home := t.TempDir() + t.Setenv("CODEAF_HOME", home) + before, _ := os.ReadFile(filepath.Join(config.ProfileDir(), "config.json")) + captureUsage(t) + for _, verb := range []string{"off", "on"} { + if code := exitCodeOf(runTelemetry([]string{verb, "--help"})); code != 0 { + t.Fatalf("`codeaf telemetry %s --help` left with %d, want 0", verb, code) + } + } + after, _ := os.ReadFile(filepath.Join(config.ProfileDir(), "config.json")) + if string(before) != string(after) { + t.Fatalf("asking for help rewrote the setting:\nbefore %q\nafter %q", before, after) + } +} + // A REAL FLAG ERROR IS STILL AN ERROR, and it is one fact said once. // // It used to be said twice: the flag package printed `flag provided but not From e95cecec677f0f7c61b583ddf65f867454f01952 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 25 Sep 2026 22:25:55 -0400 Subject: [PATCH 2/5] manual: --help prints six groups since senior-dev, and names them #1488 added a Hand it a whole task heading for the programs a build carries, so the page that said five headings was wrong on every build that carries senior-dev. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/manual/chat/running-from-the-terminal.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index 6cfa44988c..d4964a96b0 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -253,10 +253,13 @@ through the same code path the tool on the belt runs, so the two cannot drift: fetch on a keyed provider, and every `image` call the model and are billed like any other call. -## What codeaf --help prints — the five groups, and where the environment table went +## What codeaf --help prints — the six groups, and where the environment table went -`codeaf help`, `--help` and `-h` all print the same thing: every command under those five -headings, in that order, then five worked examples. +`codeaf help`, `--help` and `-h` all print the same thing: every command under six +headings, in this order — **Talk to it**, **Hand it work**, **Hand it a whole task**, +**Look at what happened**, **Housekeeping** and **Plan work by hand** — then five worked +examples. **Hand it a whole task** lists the programs this build carries, such as +`codeaf senior-dev`; a build that carries none prints the other five. **The environment table is not on that page**: it is `codeaf help env`, because it is a reference somebody consults and it used to be more than half of what `--help` printed. From e640a0925b0fd17b2a723f579f7d77a89add53cc Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 25 Sep 2026 22:53:32 -0400 Subject: [PATCH 3/5] wall: an idle tile says when it last moved on the very first read The wall's first reading of a conversation that had already finished took no new lines, so it set no fresh time, and the tile drew its spend beside a blank activity line. The manual promises one dim line saying what the conversation is doing now or when it last moved. The first reading now takes the transcript's own last write, read off the loop beside its contents, and later readings age from there. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/tui3/wall.go | 2 +- internal/tui3/wallactivity_test.go | 39 ++++++++++++++++++++++++++++++ internal/tui3/wallcontract.go | 3 +++ internal/tui3/walltail.go | 20 ++++++++++++--- 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 internal/tui3/wallactivity_test.go diff --git a/internal/tui3/wall.go b/internal/tui3/wall.go index b61151c44c..053aabd51a 100644 --- a/internal/tui3/wall.go +++ b/internal/tui3/wall.go @@ -208,7 +208,7 @@ func (a *app) wallFrame(width, height int) []string { tiles[i].rows = a.wallMiniRows(tail, wallInnerW(tileW)) if tail != nil { tiles[i].doing = wallDoing(tail.recent, tiles[i].signal) - tiles[i].moved = tail.freshAt + tiles[i].moved = tail.moved } else { tiles[i].doing = wallDoing(nil, tiles[i].signal) } diff --git a/internal/tui3/wallactivity_test.go b/internal/tui3/wallactivity_test.go new file mode 100644 index 0000000000..53a223d0c1 --- /dev/null +++ b/internal/tui3/wallactivity_test.go @@ -0,0 +1,39 @@ +package tui3 + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/Agent-Field/codeaf/internal/session" +) + +// A finished conversation's first wall reading uses the conversation's own +// last write, so opening the wall does not claim that old work happened now. +func TestWallFirstReadShowsConversationFileActivity(t *testing.T) { + now := time.Date(2026, 9, 25, 12, 0, 0, 0, time.UTC) + file := filepath.Join(t.TempDir(), "transcript.jsonl") + if err := os.WriteFile(file, []byte("saved conversation\n"), 0600); err != nil { + t.Fatal(err) + } + last := now.Add(-5 * time.Minute) + if err := os.Chtimes(file, last, last); err != nil { + t.Fatal(err) + } + agent := &fakeAgent{model: "m", past: []session.DisplayEntry{{Role: "user", Text: "hello"}, {Role: "assistant", Text: "done"}}} + a := newTestApp(agent) + a.file, a.workspace, a.title = file, filepath.Dir(file), "A finished conversation" + a.width, a.height = 120, 40 + a.clock = func() time.Time { return now } + _ = a.openWall() + drive(t, a, runCmd(a.wallReadCmd(a.frontTabKey()))...) + if got := wallTileRowsFor(t, a, a.frontTabKey()); !strings.Contains(got, "updated 5m ago") { + t.Fatalf("the first idle tile did not show its last activity:\n%s", got) + } + now = now.Add(2 * time.Minute) + if got := wallTileRowsFor(t, a, a.frontTabKey()); !strings.Contains(got, "updated 7m ago") { + t.Fatalf("the idle tile did not age with the wall clock:\n%s", got) + } +} diff --git a/internal/tui3/wallcontract.go b/internal/tui3/wallcontract.go index d581165363..aea387bba9 100644 --- a/internal/tui3/wallcontract.go +++ b/internal/tui3/wallcontract.go @@ -448,6 +448,9 @@ type wallTail struct { seen time.Time fresh int freshAt time.Time + // moved is the conversation's own last activity, including its file time + // when the first reading has no new lines to measure. + moved time.Time // spark is a ring of per-second activity, newest at sparkAt. spark [wallSparkLen]uint8 sparkAt time.Time diff --git a/internal/tui3/walltail.go b/internal/tui3/walltail.go index 8c13604acd..6d33f95f48 100644 --- a/internal/tui3/walltail.go +++ b/internal/tui3/walltail.go @@ -1,6 +1,7 @@ package tui3 import ( + "os" "strconv" "strings" "time" @@ -60,6 +61,9 @@ type wallReadMsg struct { // books is what the conversation's books said on the same trip // ([Agent.Usage]), which takes the same lock the transcript does. books float64 + // modified is the transcript file's last write, read beside its contents + // off the loop so the first idle tile has a real activity time. + modified time.Time } // wallTickMsg is the wall's clock. @@ -351,7 +355,12 @@ func (a *app) wallReadCmd(keys ...string) tea.Cmd { key := key cmds = append(cmds, func() tea.Msg { at := time.Now() - return wallReadMsg{key: key, entries: agent.Transcript(), at: at, live: live, books: agent.Usage().CostUSD} + entries := agent.Transcript() + var modified time.Time + if info, err := os.Stat(key); err == nil { + modified = info.ModTime() + } + return wallReadMsg{key: key, entries: entries, at: at, live: live, books: agent.Usage().CostUSD, modified: modified} }) } switch len(cmds) { @@ -390,6 +399,11 @@ func (a *app) wallTakeRead(msg wallReadMsg) { } tail.books = max(tail.books, msg.books) tail.take(msg.entries, msg.at, msg.live) + if !tail.freshAt.IsZero() { + tail.moved = tail.freshAt + } else if tail.moved.IsZero() { + tail.moved = msg.modified + } } // wallStir is a held conversation's stir, as the wall hears it: a read of that @@ -504,8 +518,8 @@ func (a *app) wallTiles(now time.Time) []wallTile { tile.lines = tail.lines tile.fresh = tail.fresh tile.freshAt = tail.freshAt - if !tail.freshAt.IsZero() { - tile.age = wallAge(now.Sub(tail.freshAt)) + if !tail.moved.IsZero() { + tile.age = wallAge(now.Sub(tail.moved)) } if live { tile.spark = tail.sparkline(now) From b9827a52ac6037fe1e7c57589d527bebc0925d39 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 25 Sep 2026 22:54:07 -0400 Subject: [PATCH 4/5] changelog: #1533 happy-path fixes Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/changes/unreleased/1533-happy-path-fixes.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 docs/changes/unreleased/1533-happy-path-fixes.md diff --git a/docs/changes/unreleased/1533-happy-path-fixes.md b/docs/changes/unreleased/1533-happy-path-fixes.md new file mode 100644 index 0000000000..d468c129db --- /dev/null +++ b/docs/changes/unreleased/1533-happy-path-fixes.md @@ -0,0 +1,10 @@ +--- +kind: fixed +title: telemetry --help exits 0, the manual names --help's six groups, and a wall tile says when it last moved on first read +pr: 1533 +surface: [chat, docs] +invalidates: + - "On c34a3a76d, `codeaf telemetry --help` answered `telemetry takes one of: status, info, show, on, off` and exited 1, and each telemetry verb's `--help` printed `flag: help requested` and exited 1. The group and every verb now print their usage and exit 0, and `off --help` changes nothing." + - "The manual said `codeaf --help` prints five groups. Since senior-dev (#1488) it prints six: Talk to it, Hand it work, Hand it a whole task, Look at what happened, Housekeeping, Plan work by hand. A build that carries no program prints five." + - "On c34a3a76d, the wall drew an idle conversation's tile with a blank activity line on its first read after launch. The tile now says `updated … ago` from the transcript's last write." +--- From ca58f4e64483c9063a8ed16af54ab4b2813a4484 Mon Sep 17 00:00:00 2001 From: Abir Abbas Date: Fri, 25 Sep 2026 22:54:16 -0400 Subject: [PATCH 5/5] changelog: keep #1533's title under a hundred characters Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/changes/unreleased/1533-happy-path-fixes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changes/unreleased/1533-happy-path-fixes.md b/docs/changes/unreleased/1533-happy-path-fixes.md index d468c129db..7982279074 100644 --- a/docs/changes/unreleased/1533-happy-path-fixes.md +++ b/docs/changes/unreleased/1533-happy-path-fixes.md @@ -1,6 +1,6 @@ --- kind: fixed -title: telemetry --help exits 0, the manual names --help's six groups, and a wall tile says when it last moved on first read +title: telemetry --help exits 0, the manual names six --help groups, a wall tile shows its first age pr: 1533 surface: [chat, docs] invalidates: