diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index c3203ca1..1be036e4 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1432,10 +1432,20 @@ Plans: 4. A sweep of all existing commands (framework modules and shared plugins) checks them against an Artisan feature checklist and fixes the gaps found, without changing command logic or breaking existing names and flags. The checklist covers confirmation with `--force` on destructive actions, `--pretend`/dry-run where it applies, progress bars and tables for bulk work, and `--json` output for scripting. Long-running commands such as `serve` and `queue:work` print a startup summary of what booted (environment, plugins, database and migrations state, queues, scheduler, mail, cache and realtime drivers, URLs) and report what they did on shutdown. One-shot commands end with a result line and a meaningful exit code. 5. The `diary` and `bonfire` READMEs, the docs console and logging pages, the summercms.io landing site and its roadmap presentation reflect the new console. The new code has unit tests, delivered in the phase's last plan. -**Plans:** 0 plans +**Plans:** 4 plans Plans: -- [ ] TBD (run /msd-plan-phase 19.8 to break down) +**Wave 1** +- [ ] 19.8-01-PLAN.md — charm-stack decision note, then the `diary` module (component loggers, Laravel-shaped channels, daily files, redaction) wired into the generated main; River, GORM and framework modules routed through it + +**Wave 2** *(blocked on Wave 1 completion)* +- [ ] 19.8-02-PLAN.md — `bonfire`: global flags, Artisan list and help, Examples/Hidden, JSON listing, CheckDocs, styled errors with hints and exit codes, duplicate-name rejection, SSU widgets on charm v2 + +**Wave 3** *(blocked on Wave 2 completion)* +- [ ] 19.8-03-PLAN.md — sweep of every command (checklist, hints, serve/queue:work/dev summaries and reports, http request log), docs, landing and roadmap deck; release checkpoint, then plugin commits and app pointer bumps + +**Wave 4** *(blocked on Wave 3 completion)* +- [ ] 19.8-04-PLAN.md — unit and golden tests for all new code, `scripts/check-phase19.8.sh` gate across repos, post-release fixes go-ahead ### Phase 20: Demo instance with every shared plugin diff --git a/.planning/phases/19.8-console-commands-overhaul/19.8-01-PLAN.md b/.planning/phases/19.8-console-commands-overhaul/19.8-01-PLAN.md new file mode 100644 index 00000000..b6d39acc --- /dev/null +++ b/.planning/phases/19.8-console-commands-overhaul/19.8-01-PLAN.md @@ -0,0 +1,362 @@ +--- +phase: 19.8-console-commands-overhaul +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - .planning/notes/2026-10-21-console-charm-stack.md + - .planning/research/STACK.md + - .gitignore + - go.mod + - go.sum + - README.md + - modules/bonfire/styles.go + - modules/bonfire/README.md + - modules/diary/diary.go + - modules/diary/config.go + - modules/diary/component.go + - modules/diary/console.go + - modules/diary/daily.go + - modules/diary/channels.go + - modules/diary/tracer_smoke_test.go + - modules/diary/config_smoke_test.go + - modules/diary/README.md + - internal/build/build.go + - internal/build/build_test.go + - internal/build/new.go + - internal/build/stubs/new.tmpl + - internal/build/testdata/new/main.go.golden + - internal/build/testdata/new/config__log.yaml.golden + - internal/build/testdata/new/CLAUDE.md.golden + - internal/build/testdata/new-no-plugin/CLAUDE.md.golden + - examples/hello/main.go + - examples/hello/go.sum + - examples/hello/plugins/greeter/go.sum + - docs/services/logging.md + - docs/setup/configuration.md + - docs/services/configuration.md + - modules/sunscreen/sunscreen.go + - modules/sunscreen/README.md + - modules/conga/client.go + - modules/conga/worker.go + - modules/conga/scheduler.go + - modules/conga/conga.go + - modules/conga/commands.go + - modules/conga/logger_smoke_test.go + - modules/conga/README.md + - modules/lagoon/connection.go + - modules/lagoon/purge.go + - modules/lagoon/transaction.go + - modules/lagoon/fill.go + - modules/lagoon/attach/thumb.go + - modules/lagoon/attach/bucket.go + - modules/lagoon/logger_smoke_test.go + - modules/lagoon/README.md + - modules/cabana/log.go + - modules/cabana/account.go + - modules/cabana/account_tokens.go + - modules/cabana/account_preferences.go + - modules/cabana/field_file.go + - modules/cabana/query.go + - modules/cabana/actions.go + - modules/cabana/http.go + - modules/cabana/auth.go + - modules/cabana/api_tokens.go + - modules/cabana/token_guard.go + - modules/cabana/singleton.go + - modules/cabana/reorder.go + - modules/cabana/relation_field.go + - modules/cabana/partial_render.go + - modules/cabana/field_type.go + - modules/cabana/field_dependent.go + - modules/cabana/crud.go + - modules/cabana/contracts.go + - modules/cooler/cooler.go + - modules/cooler/memory.go + - modules/cooler/rediscache.go + - modules/cooler/ratelimit.go + - modules/cooler/sitecache.go + - modules/postcard/mailer.go + - modules/postcard/drivers.go + - modules/beachcomber/beachcomber.go + - modules/lighthouse/lighthouse.go + - modules/lighthouse/centrifugo/client.go + - modules/flare/flare.go + - modules/phrasebook/translator.go + - modules/wristband/oidc_token.go + - modules/bouncer/turnstile.go +autonomous: true +requirements: [SC-1, SC-2, D-01, D-02, D-04, D-05, D-08, D-09, D-10, D-12] +user_setup: [] + +estimate: + tokens: 210000 + raw_tokens: 210000 + tasks: 3 + confidence: low + +must_haves: + truths: + - "D-02: .planning/notes/2026-10-21-console-charm-stack.md names charm.land/lipgloss/v2 v2.0.6, charm.land/bubbletea/v2 v2.1.0, charm.land/bubbles/v2 v2.2.1, charm.land/huh/v2 v2.0.3, charm.land/fang/v2 v2.0.1 and github.com/charmbracelet/colorprofile v0.4.3 (or the patch versions actually installed, recorded in the note), the v2-over-v1 reason, fang used narrowly, the own diary console handler, the stdlib daily writer, the measured binary delta of about +2.1 MB, and that only modules/bonfire and modules/diary import them; its commit is an ancestor of the first commit that makes go.mod require charm.land/lipgloss/v2." + - "D-08: diary reads log.default, log.level, log.channels (driver console, stderr, daily, stack or null, with level, format, path, days and channels) and log.components; SUMMER_LOG__* overrides that arrive as strings parse ('30' days, 'console,daily' for a stack list) and overlay the framework defaults key by key; an unknown driver, format or level fails boot with an error naming the key." + - "D-09: diary.For(name) returns a component logger that is safe in package var blocks: before diary is set up its records go to slog.Default's handler with a component attribute, after Setup they pass the component floor (log.components[name], else log.level) and reach every channel; River logs through diary.For(\"river\"), GORM through diary.For(\"db\") with parameterized SQL, and every framework module except surf's request log uses a diary component logger instead of the process default logger." + - "D-10: with no config/log.yaml an application logs to a stack of console and daily; the daily file is storage/logs/summer-YYYY-MM-DD.log under the application root, JSON, level warn, kept 14 days; summer new writes the same shape as config/log.yaml; an unwritable log directory warns once on the console and disables that channel without failing boot." + - "D-04 (logging half), D-05: the console channel renders styled lines with a fixed-width component prefix in bonfire's palette when its writer is a terminal, and logfmt (json when format: json) otherwise; SetVerbosity(1, false) lets INFO through for every component, SetVerbosity(2, false) DEBUG, and quiet raises only the console channel to WARN." + - "D-12: every channel leaf is wrapped by sunscreen.Wrap, so a password attribute reads [REDACTED] on the console and in the daily file; sunscreen.InstallDefault keeps its signature and behaviour (sunscreen.Install never existed, so no shim is added and the README says so)." + - "D-01 (palette): modules/bonfire/styles.go ports SSU's status styles (green success, cyan current, yellow warning, red and bold red error, 240 muted, bold header) to lipgloss v2, and the diary console handler takes its colors from it." + - "The generated main runs diary.Bootstrap before compass loads and diary.Setup right after, examples/hello/main.go is regenerated, and go vet ./... plus go test -short ./... stay green at every commit." + artifacts: + - path: ".planning/notes/2026-10-21-console-charm-stack.md" + provides: "D-02 dependency decision note" + contains: "charm.land/lipgloss/v2" + - path: "modules/diary/diary.go" + provides: "For, Setup, Bootstrap, SetVerbosity, Verbosity, Reset, Close" + contains: "func For(component string) *slog.Logger" + - path: "modules/diary/daily.go" + provides: "daily rotating writer with retention" + contains: "0o640" + - path: "modules/bonfire/styles.go" + provides: "shared console palette and color mode" + contains: "type Palette struct" + - path: "internal/build/stubs/new.tmpl" + provides: "summer new writes config/log.yaml" + contains: "new/config/log.yaml" + key_links: + - from: "internal/build/build.go" + to: "modules/diary/diary.go" + via: "generated main calls diary.Bootstrap then diary.Setup" + pattern: "diary.Setup\\(cfg" + - from: "modules/diary/channels.go" + to: "modules/sunscreen/sunscreen.go" + via: "every channel leaf is wrapped" + pattern: "sunscreen.Wrap\\(" + - from: "modules/conga/client.go" + to: "modules/diary/diary.go" + via: "River's logger is the river component" + pattern: "diary.For\\(\"river\"\\)" + - from: "modules/lagoon/connection.go" + to: "modules/diary/diary.go" + via: "GORM logs through the db component" + pattern: "NewSlogLogger\\(diary.For\\(\"db\"\\)" + prohibitions: + - statement: "No package outside modules/bonfire and modules/diary imports a charm.land or github.com/charmbracelet module." + status: unresolved + verification: judgment + - statement: "Nothing in this repository imports gopkg.in/yaml.v3." + status: unresolved + verification: judgment + - statement: "Framework code, READMEs and docs name no consuming application (neutral names such as acme or blog)." + status: unresolved + verification: judgment + - statement: "Nothing is pushed or tagged in this plan." + status: unresolved + verification: judgment +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format; MVP mode takes a story from `/msd mvp-phase 19.8`, which was not run, so no story is invented here): console commands in `summer` and in application binaries reach the bar set by Laravel's Artisan. The command logic stays; logging, the shared console layer in `bonfire`, and every command's help and output change. A user sees what a command did, can ask for more or less of it, and learns from `--help` how to use it without opening the docs. + +This plan's slice (plan 1 of 4, wave 1): the `diary` logging module. The charm-stack decision note lands first (project rule, D-02). Then one record from a named component logger flows from `config/log.yaml` through `compass` into a `sunscreen`-wrapped stack of console and daily file, wired into the generated `main` (the tracer). Then the full Laravel-shaped channel config with `summer new`'s `config/log.yaml` and the logging docs. Last, River, GORM and every framework module log through named components, so plan 2's verbosity flags govern third-party and framework lines alike. + +## Spec-less probe fallback + +Spec-less probe fallback skipped: phase has no requirement IDs mapped (phase_req_ids null) and no SPEC.md. This is a visible choice. `must_haves` derive from the ROADMAP success criteria SC-1..SC-5 and CONTEXT D-01..D-30; `requirements` lists those IDs for traceability. + +## Decisions taken in this plan (recorded assumptions and Claude's discretion) + +- Console handler (CONTEXT discretion): diary's own lipgloss-styled `slog.Handler`, not `charm.land/log/v2`, which fails `testing/slogtest` (RESEARCH probe). The decision note records it. +- Install order: this plan installs only `charm.land/lipgloss/v2` and `github.com/charmbracelet/colorprofile` (the palette and the console handler import them; `go mod tidy` drops unused requirements). Plan 2 installs bubbletea, bubbles, huh and fang with the code that imports them. The note names all six. +- diary imports bonfire for the palette (RESEARCH Architectural Responsibility Map: one palette for list, help, widgets and log lines). bonfire never imports diary; plan 2 connects them through a `bonfire.OnGlobals` hook. +- Framework defaults equal the approved D-08 example: `default: stack`, `level: info`, `stack` = `[console, daily]`, `console` format `auto`, `daily` at `storage/logs/summer.log`, 14 days, `json`, level `warn`, `components: {river: warn}`. The example's `surf: info` line is written as `http: info` in `summer new`'s file with a comment that `http: warn` silences the request log (D-11 and D-23 name the component `http`; plan 3 moves the request log onto it). +- Verbosity mapping (RESEARCH A4): `-v` lowers every component floor to at most INFO, `-vv` to DEBUG, `-vvv` adds source locations on the console and GORM SQL traces at INFO (parameterized). Quiet raises only the console channel to WARN; files keep their own levels. Plan 2 wires the flags; this plan provides `SetVerbosity`. +- Daily files open lazily on the first record (no empty files), in the process's local time zone (RESEARCH A7), mode 0o640 in directories created 0o750. Relative paths resolve against `Options.Root` (default: the working directory the generated main loads `config` from) and may not escape it; absolute paths are the operator's choice and accepted as given. +- Env overrides use compass's double-underscore separator (`SUMMER_LOG__CHANNELS__DAILY__DAYS=30`); D-08's `SUMMER_LOG_*` is shorthand for these. diary parses `log.*` from `Config.Lookup` by hand rather than `LoadSection` (RESEARCH A3), so env strings are accepted where YAML would give numbers or lists. +- Component names are plugin or module codes without dots (koanf splits keys on `.`). Framework components: the package name (`cabana`, `lagoon`, `attach`, `cooler`, `postcard`, `beachcomber`, `lighthouse`, `centrifugo`, `flare`, `phrasebook`, `wristband`, `bouncer`, `conga`), plus `river` and `db`. Where a module already prefers an app-published `*slog.Logger` (conga, cooler, postcard, beachcomber), that override stays first. +- `sunscreen.Install` does not exist (RESEARCH docs table); the roadmap's wording refers to `sunscreen.InstallDefault`, which keeps working unchanged as the compatibility entry point. + +## Artifacts this phase produces (this plan) + +- Planning: `.planning/notes/2026-10-21-console-charm-stack.md`; Supporting Libraries rows in `.planning/research/STACK.md` for the six charm modules. +- New module `modules/diary` (`git.golem15.com/golem15/summercms/modules/diary`): + - Functions: `For(component string) *slog.Logger`, `Bootstrap(w io.Writer, opts Options)`, `Setup(cfg *compass.Config, opts Options) error`, `SetVerbosity(verbosity int, quiet bool)`, `Verbosity() int`, `Reset()`, `Close() error`, `ParseConfig(cfg *compass.Config) (Config, error)`, `DefaultConfig() Config`, `NewConsoleHandler(w io.Writer, opts ConsoleOptions) slog.Handler`. + - Types: `Options{Verbosity int; Quiet bool; Color bonfire.ColorMode; Root string; Stderr io.Writer; Environ []string; Now func() time.Time}`, `Config{Default string; Level slog.Level; Channels map[string]ChannelConfig; Components map[string]slog.Level}`, `ChannelConfig{Driver, Format, Path string; Level slog.Level; HasLevel bool; Days int; Channels []string}`, `ConsoleOptions{Level slog.Leveler; Profile colorprofile.Profile; AddSource bool}`. + - Constants: `ComponentKey = "component"`, `DriverConsole`, `DriverStderr`, `DriverDaily`, `DriverStack`, `DriverNull`, `FormatAuto`, `FormatJSON`, `FormatLogfmt`, config keys `DefaultKey = "log.default"`, `LevelKey = "log.level"`, `ChannelsKey = "log.channels"`, `ComponentsKey = "log.components"`. + - Smoke tests: `TestDiaryTracerSmoke`, `TestConfigEnvSmoke`, `TestDailyDegradesSmoke`. +- bonfire: `ColorMode` with `ColorAuto`, `ColorAlways`, `ColorNever`; `Palette{Success, Current, Warning, Error, ErrorBold, Muted, Header lipgloss.Style}`; `DefaultPalette() Palette`; `DetectProfile(w io.Writer, mode ColorMode, environ []string) colorprofile.Profile`. +- Config keys (published): `log.default`, `log.level`, `log.channels..driver|level|format|path|days|channels`, `log.components.`. +- Generated main: `diary.Bootstrap(os.Stderr, diary.Options{})` as the first statement of `run`, `diary.Setup(cfg, diary.Options{})` after `compass.Load`; no `sunscreen` import. `summer new` writes `config/log.yaml`. +- conga: River logger `diary.For("river")`, conga's own lines `diary.For("conga")`; smoke `TestRiverLoggerUsesDiarySmoke`. lagoon: GORM logger `logger.NewSlogLogger(diary.For("db"), ...)`; smoke `TestGormLoggerUsesDiarySmoke`. +- `.gitignore` entries `/storage/` and `/examples/hello/storage/`; root README row for diary. + + +Record the charm-stack decision, then give every application one logging pipeline: named component loggers, Laravel-shaped channels configured in `config/log.yaml`, redaction on every channel, daily files on by default, and River, GORM and framework modules routed through it. + +Purpose: D-02 (note first), D-08, D-09, D-10, D-12, D-05 and the logging half of D-04, the palette half of D-01; SC-1 and the diary side of SC-2. +Output: one planning-doc commit (note + STACK.md), the diary tracer commit with READMEs and the generated main, the channel-config commit with docs and `summer new`, and one or two commits routing River, GORM and framework modules. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/19.8-console-commands-overhaul/19.8-CONTEXT.md +@.planning/phases/19.8-console-commands-overhaul/19.8-RESEARCH.md +@.planning/phases/19.8-console-commands-overhaul/19.8-PATTERNS.md +@modules/sunscreen/README.md + +Repos and concurrency: this plan writes only in summercms.go. Several sessions work here (Phase 19.5 is executing; its in-flight files are `.planning/STATE.md`, `.planning/state.json`, `scripts/check-phase19.5.sh` and 19.5 phase docs). Before each task run `git status --porcelain` on the paths the task touches; if a file this plan edits is modified by someone else, stop and report (for cabana files in Task 3: skip only those files, record them in the SUMMARY, continue with the rest). Stage files by explicit path, never `git add -A`. No co-author tags. Planning docs and code in separate commits; a README and docs change in the same commit as the API it describes. Keep `go vet ./...` and `go test -short ./...` green at every commit (and `go -C examples/hello test ./...`, a separate workspace module). Nothing is pushed or tagged. + + +Existing seams (extend, do not fork): + +- modules/sunscreen/sunscreen.go: `Wrap(next slog.Handler) slog.Handler` (idempotent: an already-wrapped handler is returned unchanged, lines 82-87), `InstallDefault(w io.Writer)` (line 94), `Scrub(s string) string`, `RedactedKeys()`; handler implements Enabled, Handle, WithAttrs, WithGroup (lines 102-125) — the shape to copy for diary's handlers. +- modules/compass/config.go: `Options{Dir, Env string; Environ []string}`, `Load(dir)`, `Open(opts)`, `(*Config).Lookup(path) (any, bool)`, `String`, `Int`, `Bool`, `Has`, `Environment()`; compass imports no framework module. Env layering: modules/compass/env.go:31-44 (`SUMMER_` prefix, `__` separates path segments, values arrive as strings). +- internal/build/build.go:94-151 `generateMain(m Manifest)`: strings.Builder template; keep every `commands = append(commands, pkg.Ctor(app...` line verbatim (cmd/summer/docs_test.go:418 regex `generatedConstructor` parses them). `formatSource` at the end. +- internal/build/build_test.go:148-170 `TestGenerateMainInstallsRedactingLogger` (asserts the sunscreen install line; replaced in Task 1). Goldens: `go test ./internal/build -run Golden -update` (flag at internal/build/new_golden_test.go:13), files under internal/build/testdata/new/ (`main.go.golden`, `config__.yaml.golden`, `CLAUDE.md.golden`) and testdata/new-no-plugin/. +- internal/build/new.go:388-391 the planned file list (`{"config/app.yaml", "new/config/app.yaml", 0o644}` and siblings); internal/build/stubs/new.tmpl `{{define "new/config/http.yaml"}}` (line 23) is the template shape; CLAUDE.md template line 126 lists the config files. +- examples/hello: committed generated `main.go`, regenerated by `go -C examples/hello run ../../cmd/summer build` (hello_test.go:282 does the same); its own go.mod plus plugin modules under plugins/ with their own go.sum; the repo go.work uses all of them. +- modules/conga/client.go:102-123 `baseConfig(s, log)` sets `river.Config.Logger`; `loggerFromApp(app)` returns the app-published `*slog.Logger` else the process default. modules/lagoon/connection.go:57 `gormFromSQL` opens GORM with `&gorm.Config{}` (GORM's own stdout logger, RESEARCH Pitfall 6). gorm v1.31.2 has `logger.NewSlogLogger(*slog.Logger, logger.Config)` (logger/slog.go:24-32). +- SSU palette to port (external repo, read-only): /media/nvme/dev/pxpx/ssu/internal/cli/tui/styles.go (Color "2" green, "6" cyan, "3" yellow, "1" red and bold red, "240" muted, bold header; drop `StyleForStatus` and the git coupling). +- Decision-note structure to copy: .planning/notes/2026-10-19-oidc-on-wristband-golang-jwt.md (Decision, Date, Status, Context, Decision bullets, rejected alternatives, Related dependencies, Consequences). +- RESEARCH Pattern 1 (proxy-backed named loggers), Pattern 2 (channels as sunscreen-wrapped leaves under slog.NewMultiHandler), Pattern 3 (daily writer), Pitfalls 4-8. + + + + + + + Task 1: Tracer: the decision note lands first, then one diary.For("journal") record flows from config/log.yaml through compass into a sunscreen-wrapped stack of console and daily file, wired into the generated main + D-02: the charm modules enter every application binary; D-08: log.* keys become published config that every application's config/log.yaml uses. + .planning/notes/2026-10-21-console-charm-stack.md, .planning/research/STACK.md, go.mod, go.sum, .gitignore, README.md, modules/bonfire/styles.go, modules/bonfire/README.md, modules/diary/diary.go, modules/diary/config.go, modules/diary/component.go, modules/diary/console.go, modules/diary/daily.go, modules/diary/channels.go, modules/diary/tracer_smoke_test.go, modules/diary/README.md, internal/build/build.go, internal/build/build_test.go, internal/build/testdata/new/main.go.golden, examples/hello/main.go, examples/hello/go.sum, examples/hello/plugins/greeter/go.sum + .planning/notes/2026-10-19-oidc-on-wristband-golang-jwt.md (structure), .planning/research/STACK.md (Supporting Libraries table, lines 28-49), 19.8-RESEARCH.md (Summary, Standard Stack, Alternatives Considered, Package Legitimacy Audit, Patterns 1-3, Pitfalls 4-8, Probe evidence), 19.8-PATTERNS.md (modules/diary section), modules/sunscreen/sunscreen.go, modules/sunscreen/README.md (README structure to copy), modules/compass/config.go, modules/compass/env.go, modules/bonfire/output.go (current color policy: NO_COLOR, TERM=dumb, FORCE_COLOR), modules/bonfire/README.md, /media/nvme/dev/pxpx/ssu/internal/cli/tui/styles.go, internal/build/build.go, internal/build/build_test.go, examples/hello/main.go, examples/hello/hello_test.go, README.md (module table rows near lines 97 and 117), .gitignore + Planning doc first (D-02). Run `go list -m -versions` (or `go list -m @latest`) for the six modules and keep RESEARCH's versions unless a newer patch of the same minor exists, in which case use and record that patch. Write `.planning/notes/2026-10-21-console-charm-stack.md` in the structure of the 2026-10-19 note: title "Decision: the console runs on charm's v2 stack, logging stays on stdlib slog"; Date 2026-10-21; Status Accepted; Context Phase 19.8 (D-01, D-02, D-05, D-08 and the CONTEXT discretion item on the console handler), 19.8-RESEARCH.md Standard Stack, Alternatives Considered, Package Legitimacy Audit and probes. Decision bullets: the modules `charm.land/lipgloss/v2` v2.0.6, `charm.land/bubbletea/v2` v2.1.0, `charm.land/bubbles/v2` v2.2.1, `charm.land/huh/v2` v2.0.3, `charm.land/fang/v2` v2.0.1 and `github.com/charmbracelet/colorprofile` v0.4.3 on the charm.land vanity paths; why v2 and not SSU's v1 pins (fang v1 already depends on a lipgloss v2 beta, so v1 widgets plus fang would compile two lipgloss majors; v1 is maintenance-only), and that SSU's widgets are ported with the v2 renames (View returns tea.View, KeyPressMsg, progress WithColors, SetWidth); only modules/bonfire (all five) and modules/diary (lipgloss, colorprofile) import them, plugins reach them through bonfire.Output and diary.For; fang is used narrowly (Execute with WithoutVersion because its -v would collide with verbosity, WithoutManpage because it adds a hidden man command, WithoutCompletions, and WithErrorHandler set to bonfire's renderer), help and list layouts are bonfire's own (D-14); colorprofile.Writer applies --ansi, --no-ansi and NO_COLOR because lipgloss v2 Render always emits ANSI; the diary console handler is our own lipgloss handler and charm's slog logger module is rejected because it fails testing/slogtest (groups treated as prefixes, empty attributes printed, empty-key groups not inlined) and duplicates diary's level and prefix model, which settles the CONTEXT discretion item; daily files use a stdlib writer of about 120 lines and lumberjack is rejected (it rotates by size, has no name-YYYY-MM-DD files, and would need its own note); fan-out is stdlib slog.NewMultiHandler and GORM uses gorm's own logger.NewSlogLogger; cost about +2.1 MB on a stripped binary (2,527,495 to 4,649,223 bytes in the probe); module go directives are at most 1.26 and fit go 1.27.0; MVS keeps cobra v1.10.2, pflag v1.0.9 and x/text v0.42.0; gopkg.in/yaml.v3 enters the module graph only as testify's indirect test dependency of fang and huh and is never imported (goccy rule); package legitimacy was checked by hand against proxy.golang.org and the charm.land go-import meta tags (origins github.com/charmbracelet/*) because the MSD seam rejects Go, verdict approved, fang noted as young (two v2 tags); install order: plan 19.8-01 installs lipgloss and colorprofile, plan 19.8-02 the other four. A rejected-alternatives paragraph (v1 pins, charm's slog logger, lumberjack, hand-written fan-out) and Consequences (every binary grows; removing the stack later means rewriting the widgets, D-01 and D-02 are rated costly; plugins never import charm modules directly). In .planning/research/STACK.md add one Supporting Libraries row per module (purpose; "Decided in .planning/notes/2026-10-21-console-charm-stack.md (Phase 19.8 D-02); imported only by modules/bonfire and modules/diary"). Commit the note and STACK.md together, alone: `docs(19.8): decide the charm v2 console stack`. + +Then the tracer. `go get charm.land/lipgloss/v2@ github.com/charmbracelet/colorprofile@`; after the code below imports them run `go mod tidy`, then `go -C examples/hello build ./...` and `go mod tidy` in every examples/hello module whose build asks for go.sum entries. + +modules/bonfire/styles.go (new): `type ColorMode int` with `ColorAuto`, `ColorAlways`, `ColorNever`; `type Palette struct` with lipgloss.Style fields `Success`, `Current`, `Warning`, `Error`, `ErrorBold`, `Muted`, `Header`; `DefaultPalette()` ported from SSU styles.go (Color "2", "6", "3", "1", "1" plus Bold, "240", Bold); `DetectProfile(w io.Writer, mode ColorMode, environ []string) colorprofile.Profile`: ColorNever gives colorprofile.NoTTY (no escape codes at all), ColorAlways gives the detected profile raised to at least ANSI256, ColorAuto treats a non-empty FORCE_COLOR as ColorAlways and otherwise uses colorprofile.Detect(w, environ) (which honours NO_COLOR and TERM=dumb). output.go is not changed in this plan (plan 2 moves the console onto the palette). bonfire README: API reference rows for the four names, Dependencies gains lipgloss and colorprofile. + +modules/diary (new package, doc comment naming D-08 and D-09). diary.go: `Options` (fields listed under Artifacts; Stderr defaults to os.Stderr, Root to ".", Now to time.Now, Environ to os.Environ()); package state in an `atomic.Pointer` to a state struct holding the root handler, the floors, the console channel's `*slog.LevelVar`, verbosity, quiet and a generation counter; `Bootstrap(w, opts)` installs a state with one console channel on w and no config (used before config loads and by the summer tool) and calls slog.SetDefault with a logger over an unnamed component handler; `Setup(cfg, opts)` parses the config, builds the channels, swaps the state, calls slog.SetDefault, and returns parse errors unchanged; `For(component)` returns `slog.New` over a component handler; `SetVerbosity(verbosity, quiet)` recomputes floors and sets the console LevelVar to WARN when quiet (otherwise back to its configured level); `Verbosity()`; `Reset()` (tests) drops the state and restores the slog default captured at the first Bootstrap or Setup; `Close()` closes open daily files. component.go: the proxy handler of RESEARCH Pattern 1: name, pre-bound attrs and groups; with no state it delegates to the process default handler (and to a plain stderr TextHandler if that default is itself a diary component handler, so it can never recurse), adding the component attribute; with a state, Enabled is `level >= floor(name)` and the root's Enabled, Handle adds `slog.String(ComponentKey, name)` when name is non-empty and replays attrs and groups onto the live root (cached per state generation); WithAttrs with a string attribute keyed `component` rebinds the name. In this task floor(name) is log.level lowered by verbosity (Task 2 adds log.components). console.go: `NewConsoleHandler(w, opts)` writing one line per record in one Write under a mutex shared by its WithAttrs and WithGroup clones: time (15:04:05, Muted), level padded to 5 (DEBUG Muted, INFO Current, WARN Warning, ERROR ErrorBold), the component attribute as a fixed-width prefix (10 columns, Header), the message, then key=value attributes (keys Muted) with groups as dotted key prefixes, empty attributes skipped and empty-key groups inlined (slogtest semantics); control characters other than tab are stripped from the message and attribute values (terminal escape injection); output goes through colorprofile.Writer with opts.Profile; AddSource prints file:line. daily.go: an unexported daily writer for `path` (dir, base name and extension split; `storage/logs/summer.log` gives files `summer-2026-10-21.log`): Write locks, picks the file by Now's local date, opens lazily with os.O_APPEND|os.O_CREATE|os.O_WRONLY and mode 0o640 after os.MkdirAll(dir, 0o750), reopens on a date change, and on each open deletes only files matching base-YYYY-MM-DD+ext whose date is more than `days` days old (days 0 keeps all); an open failure reports once through a warn callback and disables the writer (later writes succeed silently). channels.go: builds the channel named by log.default: console (format auto picks the console handler when the writer is a terminal per bonfire.DetectProfile and a TextHandler otherwise; json and logfmt force slog.JSONHandler or slog.TextHandler), daily (JSONHandler by default, TextHandler for logfmt, on the daily writer), stack (slog.NewMultiHandler over the named children); every leaf is `sunscreen.Wrap(...)` and gated by its own `*slog.LevelVar` (the channel level when set, else no extra floor). config.go: `Config`, `ChannelConfig`, constants and `DefaultConfig()` (the framework defaults listed in this plan's decisions), `ParseConfig(cfg)` reading log.default, log.level and log.channels (driver, level, format, path, days, channels) via Lookup in this task (Task 2 completes validation, stderr, null and components). + +Generated main (internal/build/build.go generateMain): import diary, drop the sunscreen import; first statement of run `diary.Bootstrap(os.Stderr, diary.Options{})`; after `compass.Load("config")` and its error check, `if err := diary.Setup(cfg, diary.Options{}); err != nil { return err }` before `backpack.New(cfg)`; every other line unchanged. Replace `TestGenerateMainInstallsRedactingLogger` with `TestGenerateMainInstallsDiary`: Bootstrap appears once and is the first statement of run before `compass.Load(`; Setup appears once between `compass.Load(` and `backpack.New(`; the sunscreen import is absent. Update the goldens with `go test ./internal/build -run Golden -update` and regenerate examples/hello/main.go with `go -C examples/hello run ../../cmd/summer build`. Add `/storage/` and `/examples/hello/storage/` to the root .gitignore (hello's tests run the binary in its directory, which now writes daily files). + +Smoke test modules/diary/tracer_smoke_test.go `TestDiaryTracerSmoke` (runs under -short): write `/config/log.yaml` with a stack of console and daily, daily level info and path storage/logs/summer.log; `compass.Open(compass.Options{Dir: /config})`; `diary.Setup(cfg, diary.Options{Root: tmp, Stderr: &buf, Color: bonfire.ColorNever, Now: fixed 2026-10-21 12:00})`; `t.Cleanup(diary.Reset)`; `diary.For("journal").Info("post saved", "password", "hunter2", "id", 7)`; assert buf holds `component=journal`, `post saved` and `[REDACTED]` and not the secret; `/storage/logs/summer-2026-10-21.log` exists with mode 0o640 and its single JSON line decodes with component journal and password `[REDACTED]`. Also assert a record from `diary.For("x")` created before Setup (package-level var in the test file) still reaches buf after Setup. + +READMEs in the same commit: modules/diary/README.md in the sunscreen structure (H1 `diary`, summary sentence "Application logging modelled on Laravel's logging config: named channels (console, stderr, daily files, stacks), per-component levels and redaction on every channel.", import line, `**Name:** A beach diary records each day; the log keeps one file per day.`, Overview, Role in the framework, Features, Usage with diary.For, API reference for every exported name above, Configuration with the default stack, Dependencies, Testing naming TestDiaryTracerSmoke); a root README.md table row with the same summary sentence and mnemonic, placed alphabetically. Neutral wording; no application named. Commit: `feat(diary): component loggers through a redacting console and daily stack`. + + go vet ./... && go test -count=1 -short ./modules/diary/... ./modules/bonfire/... ./modules/sunscreen/... ./internal/build/... && go test -count=1 -short -run '^TestDiaryTracerSmoke$' -v ./modules/diary 2>&1 | grep -q -- '^--- PASS: TestDiaryTracerSmoke' && go -C examples/hello vet ./... && go -C examples/hello test -count=1 ./... && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check + Non-zero exit: vet or a short suite is red (generated main, goldens or hello no longer match), no "--- PASS: TestDiaryTracerSmoke" line (the record did not reach both channels redacted, or the test did not run), examples/hello fails to build or test against the regenerated main, or the docs checker reports an identifier in the diary or bonfire README that does not exist. + + + - `N=$(git log --format=%H --diff-filter=A -- .planning/notes/2026-10-21-console-charm-stack.md) && A=$(git log --format=%H -S 'charm.land/lipgloss/v2' -- go.mod) && L=$(printf '%s\n' "$A" | tail -1) && test -n "$N" && test -n "$L" && git merge-base --is-ancestor "$N" "$L"` exits 0 (note committed before go.mod requires lipgloss). + - `for s in charm.land/lipgloss/v2 charm.land/bubbletea/v2 charm.land/bubbles/v2 charm.land/huh/v2 charm.land/fang/v2 github.com/charmbracelet/colorprofile WithoutVersion slogtest lumberjack '2.1 MB'; do grep -qF -- "$s" .planning/notes/2026-10-21-console-charm-stack.md || exit 1; done` exits 0. + - `grep -c '2026-10-21-console-charm-stack.md' .planning/research/STACK.md` prints at least 1. + - `go list -f '{{.ImportPath}} {{join .Imports " "}}' ./... | grep -E ' (charm\.land/|github\.com/charmbracelet/)' | grep -vcE '^git\.golem15\.com/golem15/summercms/modules/(bonfire|diary) '` prints 0. + - `grep -c 'sunscreen.Wrap(' modules/diary/channels.go` prints at least 1 and `grep -c '0o640' modules/diary/daily.go` prints at least 1. + - `grep -c 'diary.Bootstrap(os.Stderr' internal/build/testdata/new/main.go.golden` prints 1, `grep -c 'diary.Setup(cfg' examples/hello/main.go` prints 1, and `grep -c 'modules/sunscreen"' examples/hello/main.go` prints 0. + - `grep -c '\[diary\](modules/diary/README.md)' README.md` prints 1 and `grep -c '^\*\*Name:\*\*' modules/diary/README.md` prints 1. + - `git check-ignore -q examples/hello/storage/logs/summer-2026-10-21.log` exits 0. + + The dependency decision is on record before any charm code, and a record from a named component logger reaches a styled-or-logfmt console line and a JSON daily file, both redacted, in every generated application. + + + + Task 2: Operators configure logging the Laravel way: stderr and null drivers, per-channel level and format, log.components, env overrides as strings, a confined path and a daily channel that degrades; summer new writes config/log.yaml and the docs describe it + D-08: the log.* key shape and its defaults are published in summer new's config/log.yaml and the docs; changing them later breaks every application's file. + modules/diary/config.go, modules/diary/channels.go, modules/diary/component.go, modules/diary/daily.go, modules/diary/config_smoke_test.go, modules/diary/README.md, internal/build/new.go, internal/build/stubs/new.tmpl, internal/build/testdata/new/config__log.yaml.golden, internal/build/testdata/new/CLAUDE.md.golden, internal/build/testdata/new-no-plugin/CLAUDE.md.golden, docs/services/logging.md, docs/setup/configuration.md, docs/services/configuration.md + modules/diary/config.go, modules/diary/channels.go, modules/diary/component.go, modules/diary/daily.go (from Task 1), modules/conga/client.go:34-60 (settingsFromApp: defaults first, tolerant string-or-int parsing), modules/cooler/cooler.go:297-307 (errors that name the key), modules/compass/env.go, internal/build/new.go (planNewApp list near line 388), internal/build/stubs/new.tmpl (config templates and the CLAUDE.md template line 126), internal/build/testdata/new/config__http.yaml.golden, docs/services/logging.md, docs/setup/configuration.md, docs/services/configuration.md, 19.8-CONTEXT.md (D-08 example YAML, D-10), 19.8-RESEARCH.md (Pitfalls 4 and 8, Security Domain V5 and V12) + Per D-08, D-09 and D-10. config.go `ParseConfig(cfg)`: start from `DefaultConfig()` and overlay what the config holds, channel by channel and key by key, so a single env override such as `SUMMER_LOG__CHANNELS__DAILY__DAYS=30` with no log.yaml keeps every other default. Rules: log.level and every level value accept debug, info, warn, warning, error (case-insensitive); log.default must name a channel; `log.channels..driver` must be one of console, stderr, daily, stack, null (a channel that is not a default and has no driver is an error naming `log.channels..driver`); format must be auto, json or logfmt, auto only for console (stderr defaults to logfmt, daily to json); days accepts an int or a decimal string, at least 0; channels accepts a YAML list or a comma-separated string, trimmed; `log.components.` values are levels. Every error names the full key, for example `diary: log.channels.daily.days must be a whole number of at least 0`. Stacks may nest; unknown child names and cycles are errors naming the stack. + +channels.go: add `stderr` (TextHandler or JSONHandler on Options.Stderr, never styled) and `null` (slog.DiscardHandler); each still wrapped by sunscreen.Wrap. Path handling for daily: a relative path is cleaned and joined to Options.Root, and one that escapes Root (a `..` prefix after cleaning) is a config error naming `log.channels..path`; an absolute path is used as given. daily.go: when opening fails (unwritable or read-only directory), the warn callback logs one WARN record from component `diary` on the console channel naming the path and the error, and the channel is disabled; Setup never fails because of a log file. component.go: floor(name) is `Components[name]` when set, else `Level`; verbosity 1 lowers every floor to at most INFO, 2 and more to DEBUG; records from loggers without a component use `Level`. + +summer new: add `{"config/log.yaml", "new/config/log.yaml", 0o644}` to the planned list in internal/build/new.go after storage.yaml, and a `{{define "new/config/log.yaml"}}` template in new.tmpl holding the D-08 example with short comments: console is styled on a terminal and json or logfmt otherwise; the daily file rotates per day under storage/logs and is kept 14 days; components lists `river: warn` and `http: info` with the comment that `http: warn` silences the request log; env overrides use `SUMMER_LOG__` with double underscores. Add `log.yaml` to the config-file list in the CLAUDE.md template. Update goldens with `go test ./internal/build -run Golden -update` (a new `config__log.yaml.golden`; CLAUDE.md goldens). + +Docs in the same commit. docs/services/logging.md: keep its front matter; rewrite "The redacting default logger" into "Channels" (table: console, stderr, daily, stack, null and what each writes), "config/log.yaml" (the D-08 example, defaults when the file is absent, the Laravel config/logging.php comparison), "Component loggers" (diary.For, name rule: plugin or module code without dots, levels per component), "Daily files" (naming, retention, 0640 mode, path relative to the application root, an unwritable directory disables the channel with one warning), "Redaction" (sunscreen.Wrap on every channel; sunscreen.InstallDefault remains for code that installs its own default). Keep "Request log" and "Panics" unchanged (plan 3 updates the request log). docs/setup/configuration.md and docs/services/configuration.md: the log keys and an example `SUMMER_LOG__CHANNELS__DAILY__DAYS=30` (double underscore; string values accepted). diary README: Configuration section with every key and its default, Features gains stderr, null and components; Testing names the new smoke tests. + +Smoke tests (config_smoke_test.go, -short): `TestConfigEnvSmoke` builds a compass config from a temp dir with no log.yaml and an Environ of `SUMMER_LOG__CHANNELS__DAILY__DAYS=30`, `SUMMER_LOG__CHANNELS__STACK__CHANNELS=console,daily`, `SUMMER_LOG__COMPONENTS__RIVER=error`, and asserts the parsed Config (days 30, stack children console and daily, river ERROR, every other default intact) plus one error case per rule naming its key. `TestDailyDegradesSmoke` points daily at a directory made read-only (skip when running as root) and asserts Setup returns nil, one warning line naming the path reaches the console buffer, and a later record still reaches the console. Commit: `feat(diary): channels, components and config/log.yaml`. + + go vet ./... && go test -count=1 -short ./modules/diary/... ./internal/build/... && go test -count=1 -short -run '^(TestConfigEnvSmoke|TestDailyDegradesSmoke|TestDiaryTracerSmoke)$' -v ./modules/diary 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 3 && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check + Non-zero exit, or the count of top-level "--- PASS: Test" lines is not 3 (an env-string or error case failed, the degrade test failed or skipped, or a name did not run), or the new-app goldens or the docs checker disagree with the code. + + + - `grep -c 'define "new/config/log.yaml"' internal/build/stubs/new.tmpl` prints 1 and `test -f internal/build/testdata/new/config__log.yaml.golden` exits 0. + - `grep -cE '^ *driver: (stack|console|daily)' internal/build/testdata/new/config__log.yaml.golden` prints at least 3 and `grep -c 'http: info' internal/build/testdata/new/config__log.yaml.golden` prints 1. + - `grep -cE 'DriverStderr|DriverNull' modules/diary/config.go` prints at least 2. + - `grep -c 'SUMMER_LOG__' docs/setup/configuration.md` prints at least 1 and `grep -c 'diary.For' docs/services/logging.md` prints at least 1. + - `grep -c 'log.channels' modules/diary/README.md` prints at least 1 and `grep -c 'log.components' modules/diary/README.md` prints at least 1. + + An operator can shape logging entirely from config/log.yaml or SUMMER_LOG__ variables, bad keys fail with the key named, an unwritable log directory never stops boot, and new applications ship the file. + + + + Task 3: River, GORM and every framework module log through named diary components, so log.components and the coming verbosity flags govern third-party and framework lines alike + modules/conga/client.go, modules/conga/worker.go, modules/conga/scheduler.go, modules/conga/conga.go, modules/conga/commands.go, modules/conga/logger_smoke_test.go, modules/conga/README.md, modules/lagoon/connection.go, modules/lagoon/purge.go, modules/lagoon/transaction.go, modules/lagoon/fill.go, modules/lagoon/attach/thumb.go, modules/lagoon/attach/bucket.go, modules/lagoon/logger_smoke_test.go, modules/lagoon/README.md, modules/cabana/log.go, modules/cabana/account.go, modules/cabana/account_tokens.go, modules/cabana/account_preferences.go, modules/cabana/field_file.go, modules/cabana/query.go, modules/cabana/actions.go, modules/cabana/http.go, modules/cabana/auth.go, modules/cabana/api_tokens.go, modules/cabana/token_guard.go, modules/cabana/singleton.go, modules/cabana/reorder.go, modules/cabana/relation_field.go, modules/cabana/partial_render.go, modules/cabana/field_type.go, modules/cabana/field_dependent.go, modules/cabana/crud.go, modules/cabana/contracts.go, modules/cooler/cooler.go, modules/cooler/memory.go, modules/cooler/rediscache.go, modules/cooler/ratelimit.go, modules/cooler/sitecache.go, modules/postcard/mailer.go, modules/postcard/drivers.go, modules/beachcomber/beachcomber.go, modules/lighthouse/lighthouse.go, modules/lighthouse/centrifugo/client.go, modules/flare/flare.go, modules/phrasebook/translator.go, modules/wristband/oidc_token.go, modules/bouncer/turnstile.go, modules/sunscreen/sunscreen.go, modules/sunscreen/README.md, modules/diary/README.md, docs/services/logging.md + modules/conga/client.go (baseConfig :102-110, loggerFromApp :116-123), modules/lagoon/connection.go (gormFromSQL :56-75), modules/cooler/cooler.go (loggerFromApp :384-390, warner :400-440), modules/postcard/mailer.go (:150-161), modules/beachcomber/beachcomber.go (:130-182), modules/cabana/auth.go (:390-396), modules/bouncer/turnstile.go (:225-240, the turnstile verification log lines) and modules/bouncer/turnstile_test.go (TestTurnstileNeverLogsSecret :348, captures through slog.SetDefault), modules/sunscreen/sunscreen.go (InstallDefault doc :89-99), modules/sunscreen/README.md, docs/services/logging.md ("Logging from a plugin"), modules/diary/diary.go (For and the no-state fallback), 19.8-RESEARCH.md (Pitfalls 5 and 6, Don't Hand-Roll row "GORM to slog"), 19.8-PATTERNS.md (Shared Patterns: Logger lookup) + Per D-09 and D-12. Before editing, run `git status --porcelain -- modules/cabana modules/lagoon modules/conga modules/cooler modules/postcard modules/bouncer`; files another session has modified are skipped and listed in the SUMMARY. + +River and GORM first (commit `feat(conga,lagoon): River and GORM log through diary components`). conga/client.go: split the lookup into `riverLogger(app)` (the app-published `*slog.Logger` when present, else `diary.For("river")`) used by `baseConfig`, and conga's own lines through the published logger else `diary.For("conga")`; the framework default `components: {river: warn}` keeps River quiet at INFO. lagoon/connection.go gormFromSQL: `&gorm.Config{Logger: gormLogger()}` where unexported `gormLogger()` returns `logger.NewSlogLogger(diary.For("db"), logger.Config{SlowThreshold: 200 * time.Millisecond, LogLevel: logger.Warn, IgnoreRecordNotFoundError: true, ParameterizedQueries: true})`, with LogLevel `logger.Info` when `diary.Verbosity() >= 3` (SQL traces only at -vvv; parameterized so values never reach the log). README lagoon: a Logging paragraph (GORM lines leave stdout and arrive as component db, record-not-found is no longer logged, -vvv shows parameterized SQL); README conga: River logs as component river (warn by default). + +Then framework modules (commit `refactor: framework modules log through diary components`). Every non-test call that logs through the process default logger in the files listed above moves to a package-level component logger: add an unexported `var moduleLog = diary.For("")` in one file per package (modules/cabana/log.go for cabana; modules/bouncer/turnstile.go for bouncer, whose turnstile middleware holds its only two log calls; in the existing main file of the other packages), component name = package name (cabana, lagoon, attach, cooler, postcard, beachcomber, lighthouse, centrifugo, flare, phrasebook, wristband, bouncer, conga). Where a module prefers an app-published `*slog.Logger` (conga, cooler, postcard, beachcomber `Service.Logger` and `loggerFromApp`), keep that override first and fall back to the component logger. cooler's warner keeps its setLogger seam and starts from `moduleLog`. Messages and attributes do not change (cabana's `admin.auth` audit record keeps its message and gains only the component attribute). modules/surf/accesslog.go is not touched (plan 3 moves the request log to component http, D-23). Tests that install a capture handler with slog.SetDefault keep working because a component logger without diary setup delegates to the process default handler; if a test compares the exact attribute set, add the component attribute to its expectation. + +sunscreen (same refactor commit): the InstallDefault doc comment and README Overview say that generated applications now install diary, which wraps every channel with sunscreen.Wrap, and that InstallDefault stays as the compatibility entry point for programs that install their own default (D-12; note that no sunscreen.Install function exists). diary README Role section lists River (river), GORM (db) and the framework components. docs/services/logging.md "Logging from a plugin": plugins call `diary.For("")` instead of the process default logger; the logger is safe to create in a package var. + +Smoke tests (-short): modules/conga/logger_smoke_test.go `TestRiverLoggerUsesDiarySmoke`: `diary.Bootstrap(&buf, diary.Options{Color: bonfire.ColorNever})`, `t.Cleanup(diary.Reset)`, `riverLogger(nil).Warn("probe")` and buf contains `component=river`; with an app that publishes a capture logger the capture receives it instead. modules/lagoon/logger_smoke_test.go `TestGormLoggerUsesDiarySmoke`: same setup, `gormLogger().Warn(ctx, "slow %s", "query")` and buf contains `component=db`; `diary.SetVerbosity(3, false)` then a fresh gormLogger reports logger.Info as its level (assert via the returned logger.Interface's LogMode comparison or by logging a Trace and seeing a line). + + go vet ./... && go test -count=1 -short ./... && go test -count=1 -short -run '^TestRiverLoggerUsesDiarySmoke$' -v ./modules/conga 2>&1 | grep -q -- '^--- PASS: TestRiverLoggerUsesDiarySmoke' && go test -count=1 -short -run '^TestGormLoggerUsesDiarySmoke$' -v ./modules/lagoon 2>&1 | grep -q -- '^--- PASS: TestGormLoggerUsesDiarySmoke' && go -C examples/hello test -count=1 ./... && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check + Non-zero exit: a module suite is red after the logger move (for example a capture test now misses records), no "--- PASS" line for either smoke test (River or GORM records do not carry their component, or the test did not run), hello fails, or the docs checker rejects an identifier. + + + + - `grep -rn 'slog.Default()' modules --include=*.go | grep -v '_test.go' | grep -v '^modules/surf/accesslog.go' | grep -v '^modules/diary/' | wc -l` prints 0 (files skipped because another session had them modified are listed in the SUMMARY and excluded from this count by name). + - `grep -rnE 'slog\.(Info|Warn|Error|Debug)(Context)?\(' modules --include=*.go | grep -v '_test.go' | grep -v '^modules/diary/' | wc -l` prints 0. + - `grep -c 'diary.For("river")' modules/conga/client.go` prints at least 1 and `grep -c 'NewSlogLogger(diary.For("db")' modules/lagoon/connection.go` prints 1. + - `grep -c 'ParameterizedQueries: true' modules/lagoon/connection.go` prints 1. + - `grep -c 'InstallDefault' modules/sunscreen/README.md` prints at least 1 and `grep -c 'diary' modules/sunscreen/README.md` prints at least 1. + + River, GORM and every framework module write through named diary components, so their lines carry a component, obey log.components and verbosity, and are redacted on every channel. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| application and plugin code to log channels | attribute values may carry user data, tokens or SQL parameters | +| operator config and environment to diary | log.yaml and SUMMER_LOG__ variables choose paths, levels and drivers | +| log files on the host | daily files are read by other host users and by backups | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.8-01 | Information disclosure | diary channels (channels.go) | high | mitigate | every leaf wrapped by sunscreen.Wrap, including stderr, daily and null; TestDiaryTracerSmoke asserts [REDACTED] on the console and in the daily file; plan 4 asserts it per driver | +| T-19.8-02 | Information disclosure | GORM logger (lagoon/connection.go) | medium | mitigate | ParameterizedQueries true; SQL traces only at -vvv; record-not-found not logged; values never interpolated into log lines | +| T-19.8-03 | Tampering | console handler (console.go) | medium | mitigate | control characters other than tab stripped from messages and attribute values before writing to a terminal, so logged user data cannot inject escape sequences | +| T-19.8-04 | Tampering | daily path (log.channels.*.path) | low | mitigate | relative paths cleaned and confined to the application root, an escaping path is a config error naming the key; absolute paths are an explicit operator choice | +| T-19.8-05 | Information disclosure | daily files (daily.go) | medium | mitigate | files 0o640 and directories 0o750; retention deletes only base-YYYY-MM-DD+ext files | +| T-19.8-06 | Denial of service | daily writer | medium | mitigate | lazy open, retention days (14 by default), an unwritable directory warns once and disables the channel, boot never fails on a log file (TestDailyDegradesSmoke) | +| T-19.8-SC | Tampering | Go module installs (go.mod) | high | mitigate | versions pinned from the RESEARCH legitimacy audit (proxy.golang.org and charm.land go-import origins), decision note committed first, go.sum verified by the toolchain, acceptance criterion proves only bonfire and diary import charm modules | + + + +- Task 1: the note precedes go.mod's lipgloss requirement; TestDiaryTracerSmoke; the generated-main test, goldens and examples/hello; docs checker. +- Task 2: TestConfigEnvSmoke, TestDailyDegradesSmoke; summer new goldens; docs checker. +- Task 3: the whole short suite; TestRiverLoggerUsesDiarySmoke and TestGormLoggerUsesDiarySmoke; no framework module logs through the process default logger except surf's request log. +- Feedback sampling: after each commit run `go test -short ./modules/diary/... ./modules/bonfire/... ./internal/build/...`; after the plan, `go vet ./... && go test -short ./... && go -C examples/hello test ./...`. + + + +- D-02: decision note on record before any charm import. +- D-08, D-10: Laravel-shaped config with framework defaults, env strings, daily files on by default, summer new's config/log.yaml. +- D-09, D-12: component loggers everywhere, redaction on every channel, InstallDefault unchanged. +- D-04 and D-05 (logging): styled on a terminal, logfmt or json otherwise; SetVerbosity ready for plan 2. + + + +Create `.planning/phases/19.8-console-commands-overhaul/19.8-01-SUMMARY.md` when done, with the commits in order (planning doc first), the charm versions actually installed, the final exported diary and bonfire names (plan 2 builds on them), any cabana or other files skipped because another session had them modified, and any deviation from this plan's names. + diff --git a/.planning/phases/19.8-console-commands-overhaul/19.8-02-PLAN.md b/.planning/phases/19.8-console-commands-overhaul/19.8-02-PLAN.md new file mode 100644 index 00000000..2d83a5a2 --- /dev/null +++ b/.planning/phases/19.8-console-commands-overhaul/19.8-02-PLAN.md @@ -0,0 +1,347 @@ +--- +phase: 19.8-console-commands-overhaul +plan: 02 +type: execute +wave: 2 +depends_on: ["19.8-01"] +files_modified: + - go.mod + - go.sum + - modules/bonfire/command.go + - modules/bonfire/root.go + - modules/bonfire/globals.go + - modules/bonfire/version.go + - modules/bonfire/execute.go + - modules/bonfire/list.go + - modules/bonfire/help.go + - modules/bonfire/docs.go + - modules/bonfire/errors.go + - modules/bonfire/styles.go + - modules/bonfire/output.go + - modules/bonfire/widgets.go + - modules/bonfire/prompts.go + - modules/bonfire/live.go + - modules/bonfire/selector.go + - modules/bonfire/helpers.go + - modules/bonfire/list_smoke_test.go + - modules/bonfire/help_smoke_test.go + - modules/bonfire/widgets_smoke_test.go + - modules/bonfire/README.md + - modules/diary/diary.go + - modules/diary/console.go + - modules/diary/README.md + - internal/build/build.go + - internal/build/build_test.go + - internal/build/testdata/new/main.go.golden + - examples/hello/main.go + - examples/hello/hello_test.go + - examples/hello/go.sum + - examples/hello/plugins/greeter/go.sum + - cmd/summer/main.go + - cmd/summer/docs.go + - internal/docsite/check_commands.go + - docs/console/introduction.md + - docs/console/writing-commands.md + - docs/services/logging.md + - .planning/todos/pending/bonfire-duplicate-command-names.md + - .planning/todos/done/bonfire-duplicate-command-names.md +autonomous: true +requirements: [SC-2, SC-3, D-01, D-02, D-03, D-04, D-13, D-14, D-15, D-16, D-17, D-18, D-27, D-28, D-30] +user_setup: [] + +estimate: + tokens: 230000 + raw_tokens: 230000 + tasks: 3 + confidence: low + +must_haves: + truths: + - "D-13: running an application binary or summer with no arguments (or with only -h/--help) prints the Artisan list: a 'SummerCMS (framework )' header from debug.ReadBuildInfo, Usage, the global options, then 'Available commands:' with top-level commands first and namespaces (the part before the first colon) sorted under their own header lines, descriptions aligned; exit code 0." + - "SC-2, D-04: every command accepts -v/-vv/-vvv (--verbose), -q/--quiet, --ansi/--no-ansi, -n/--no-interaction and --env; bonfire.PeekGlobals reads them before config loads so --env picks the compass environment and diary starts at the right verbosity, and the root's OnGlobals hook re-applies them to diary after cobra parses; a bonfire.Call root never runs the hook." + - "D-14, D-15: ' help ', 'summer help ' and ' --help' print Description, Usage, Arguments, Options with [default: ...], Global options, Help (Long) and Examples; bonfire.Command gains Examples []Example and Hidden bool at the end of the struct, so existing plugins compile unchanged; Hidden commands run but are left out of list unless list --all." + - "D-16, D-17: 'list --format=json' prints every command (hidden ones flagged) with arguments, options, defaults and examples as a bonfire.Listing; bonfire.CheckDocs(commands) returns one DocProblem per command lacking a description, long help, examples, or an argument or option description." + - "D-18: errors render as a styled block on stderr: ERROR and the cause, the explanation and a Try: line from bonfire.WithHint (with {bin} replaced by bin/ or summer), 'Run with -v for details.' at default verbosity and the wrapped chain at -v; cobra's 'Did you mean' becomes the Try line; bonfire.Execute returns 2 for usage errors (unknown command or flag, wrong argument count), 1 for failures, and prints nothing more for errors wrapped by bonfire.Reported; message text passes through sunscreen.Scrub." + - "D-01, D-02, D-30: on a terminal, Spinner, Progress, bonfire.Process, Table and Choice render through ported SSU widgets on bubbletea/bubbles/lipgloss v2 (spinner, multi-row process view, gradient progress bar, single arrow-key selector with a confirmation screen; no multi-select), Ask/Confirm/Secret use huh; fang.Execute runs with WithoutVersion, WithoutManpage, WithoutCompletions and WithErrorHandler; the Output interface is unchanged." + - "D-03, D-27, D-28: off a terminal, under -q, --no-ansi or -n every widget prints plain lines without animation and today's non-TTY bytes are unchanged; -q suppresses Info, Success, Warning, spinners and progress but keeps Println/Printf/Write, tables, --json data and Error; -q implies -n; with -n prompts return their defaults and Secret without piped stdin fails with a hint; bonfire.ConfirmProceed returns true without asking whenever the Output is not interactive." + - "Folded todo: bonfire.NewRoot fails with ErrDuplicateCommand naming the command when two commands share a name (or a command is named list or help), and with ErrReservedFlag when a command flag reuses a global name or shorthand; the bonfire README and docs/console/writing-commands.md say so and the todo moves to .planning/todos/done/." + artifacts: + - path: "modules/bonfire/globals.go" + provides: "Globals, PeekGlobals, Globals.Args" + contains: "func PeekGlobals(args []string) Globals" + - path: "modules/bonfire/execute.go" + provides: "Execute, Fail, exit codes, narrow fang" + contains: "fang.WithoutVersion()" + - path: "modules/bonfire/list.go" + provides: "Artisan list, list --all, list --format=json, Listing" + contains: "Available commands:" + - path: "modules/bonfire/help.go" + provides: "Artisan per-command help" + contains: "Examples:" + - path: "modules/bonfire/live.go" + provides: "ported spinner, process and progress models (bubbletea v2)" + contains: "tea.WithInput(nil)" + key_links: + - from: "internal/build/build.go" + to: "modules/bonfire/execute.go" + via: "generated main returns bonfire.Execute's exit code" + pattern: "bonfire.Execute\\(" + - from: "internal/build/build.go" + to: "modules/diary/diary.go" + via: "PeekGlobals feeds diary before compass, OnGlobals re-applies after parsing" + pattern: "bonfire.OnGlobals\\(diary.ApplyGlobals\\)" + - from: "modules/diary/console.go" + to: "modules/bonfire/helpers.go" + via: "log lines print above a running live view" + pattern: "bonfire.PrintAbove\\(" + prohibitions: + - statement: "No method is added to bonfire.Output, Input, Progress or Command's existing fields; new behaviour arrives through added struct fields, optional interfaces and helper functions only." + status: unresolved + verification: judgment + - statement: "No prompt blocks a run whose stdin or stdout is not a terminal, or that has -n or -q." + status: unresolved + verification: judgment + - statement: "Framework code, READMEs and docs name no consuming application." + status: unresolved + verification: judgment + - statement: "Nothing is pushed or tagged in this plan." + status: unresolved + verification: judgment +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format; no story is invented, see plan 01): console commands reach the Artisan bar; a user sees what a command did, can ask for more or less of it, and learns from `--help` how to use it. + +This plan's slice (plan 2 of 4, wave 2): the shared console layer in `bonfire`. The tracer makes a bare application binary print the Artisan list, carries the global flags into `diary` before plugins boot, and returns exit codes through `bonfire.Execute` in the generated `main` and in `summer`. Then per-command help, `help `, Examples and Hidden, the JSON listing, the documentation coverage helper and the styled error block with hints. Last, SSU's widgets ported to charm v2 behind the unchanged `Output` interface, with the degradation rules for non-TTY, `-q`, `--no-ansi` and `-n`. + +## Spec-less probe fallback + +Spec-less probe fallback skipped: no requirement IDs (phase_req_ids null) and no SPEC.md; visible choice. `must_haves` cite SC-2, SC-3 and D-01..D-30. + +## Decisions taken in this plan (recorded assumptions and Claude's discretion) + +- Global flag semantics (CONTEXT discretion, following Artisan and Symfony): `-q` implies `-n` (Symfony sets non-interactive when quiet); `--ansi` and `--no-ansi` are last-wins; `--env X` and `--env=X` both work and reach `compass.Options.Env`, which already validates the value; `-v` counts up to 3. +- Usage name: `summer` for the tool, `bin/` for application binaries (the D-13 preview). Example commands are stored without the binary (`admin:token:create jane --json`) and rendered with the usage name, so one Example serves both `summer` delegates and the application binary. +- Per-command help puts the command's own options under "Options:" (the D-14 preview) and the global flags in a separate compact "Global options:" block, then "Help:" with Long, then "Examples:". The preview's Options block stays exactly as approved; the extra blocks teach the global flags and show the long help SC-3 requires. +- Defaults render as `[default: "x"]`, unquoted when the default parses as an integer (`[default: 90]`, D-14 preview); bare flags show no default; repeatable flags add "(multiple values allowed)". +- fang (D-02) is used through `fang.Execute` with `WithoutVersion`, `WithoutManpage`, `WithoutCompletions` and `WithErrorHandler`; `-h`/`--help` without a command is routed to the list before fang runs (RESEARCH Pattern 5, option b); every subcommand gets its own help func, which cobra prefers over the root's. +- Choice uses the ported single selector with SSU's confirmation screen (D-01, D-30); Ask, Confirm and Secret use huh (D-02). Multi-select, the filter and the detail pane are not ported (D-30). +- `ConfirmProceed` encodes D-28 for plan 3: it asks only on an interactive terminal; off a terminal, under `-n`/`-q` or in `bonfire.Call` it returns true without asking, so deploys over SSH never block. +- `-n` with Secret: when stdin is piped the line is read (admin:create in scripts keeps working); when stdin is a terminal the prompt fails with a hint to pipe the value, because Secret has no default. + +## Artifacts this phase produces (this plan) + +- bonfire (`git.golem15.com/golem15/summercms/modules/bonfire`): + - Types: `Globals{Verbosity int; Quiet bool; Color ColorMode; NoInteraction bool; Env string}`, `RootOption`, `Version{App, Framework string; Local bool}`, `Example{Command, Description string}`, `Listing{Application ApplicationInfo; GlobalOptions []OptionInfo; Commands []CommandInfo}`, `ApplicationInfo`, `CommandInfo`, `ArgumentInfo`, `OptionInfo`, `ExampleInfo`, `DocProblem{Command string; Missing []string}`, `HintError{Err error; Explanation, Try string}`, `UsageError{Err error}`, `ItemProgress` (optional interface: Progress plus `Current(item string)` and `Fail(item string, err error)`). + - Command fields (appended): `Examples []Example`, `Hidden bool`. + - Functions: `PeekGlobals(args []string) Globals`, `(Globals).Args() []string`, `WithVersion(v Version) RootOption`, `OnGlobals(fn func(Globals)) RootOption`, `BuildVersion() Version`, `(Version).Header(name string) string`, `Execute(ctx context.Context, root *cobra.Command, args []string) int`, `Fail(w io.Writer, name string, err error, g Globals) int`, `RenderError(w io.Writer, name string, err error, g Globals)`, `WithHint(err error, explanation, try string) error`, `Reported(err error) error`, `Usage(err error) error`, `Describe(name string, commands []Command, v Version) Listing`, `CheckDocs(commands []Command) []DocProblem`, `GlobalsOf(out Output) Globals`, `VerbosityOf(out Output) int`, `IsQuiet(out Output) bool`, `Interactive(out Output) bool`, `ConfirmProceed(out Output, question string) (bool, error)`, `Process(ctx context.Context, out Output, operation string, items []string, fn func(ctx context.Context, item string) error) error`, `Current(p Progress, item string)`, `Failed(p Progress, item string, err error)`, `PrintAbove(line string) bool`. + - Variables and constants: `ExitOK = 0`, `ExitFailure = 1`, `ExitUsage = 2`, `ErrDuplicateCommand`, `ErrReservedFlag`, `ErrReported`, `ErrAborted`. + - CLI: `list` (`--all`, `--format=txt|json`), `help [command]`; persistent flags `-v|--verbose` (count), `-q|--quiet`, `--ansi`, `--no-ansi`, `-n|--no-interaction`, `--env`. + - Smoke tests: `TestListTracerSmoke`, `TestHelpSmoke`, `TestListJSONSmoke`, `TestErrorRenderSmoke`, `TestWidgetMatrixSmoke`. +- diary: `ApplyGlobals(g bonfire.Globals)`, `FromGlobals(g bonfire.Globals) Options`; the console handler prints through `bonfire.PrintAbove` while a live view runs. +- Generated main: `func main() { os.Exit(run(os.Args[1:], os.Stdout)) }`, `func run(args []string, out io.Writer) int` with `bonfire.PeekGlobals`, `compass.Open(compass.Options{Dir: "config", Env: g.Env})`, `bonfire.Fail` before the root exists, `bonfire.NewRoot(..., bonfire.WithVersion(bonfire.BuildVersion()), bonfire.OnGlobals(diary.ApplyGlobals))`, `bonfire.Execute`. +- examples/hello: `TestBuiltBinaryListsCommands`; updated `TestHelpListsServe`, `TestMigrateWithoutDSNFailsLoudly`, `TestBuiltBinaryUnknownCommandExitsNonZero` (exit 2). +- docs checker: `commandWord` knows the bool globals and `--env`; `docsCommands` lists `list` and `help` for the tool and applications. +- Dependencies installed: `charm.land/fang/v2`, `charm.land/bubbletea/v2`, `charm.land/bubbles/v2`, `charm.land/huh/v2` at the decision note's versions. + + +Make bonfire Artisan-grade: global flags on every command that reach diary before boot, the grouped list and per-command help with examples, a JSON listing and a coverage helper, styled errors with hints and exit codes, duplicate-name rejection, and SSU's widgets on charm v2 that degrade cleanly, all behind the unchanged Output interface. + +Purpose: SC-2, SC-3 (framework side), D-01, D-02 (rest of the stack), D-03, D-04 (flags), D-13..D-18, D-27, D-28 (helper), D-30, folded todo bonfire-duplicate-command-names. +Output: three code commits with README and docs, one planning-doc commit closing the todo. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/19.8-console-commands-overhaul/19.8-CONTEXT.md +@.planning/phases/19.8-console-commands-overhaul/19.8-RESEARCH.md +@.planning/phases/19.8-console-commands-overhaul/19.8-PATTERNS.md +@.planning/phases/19.8-console-commands-overhaul/19.8-01-SUMMARY.md +@modules/bonfire/README.md + +Repos and concurrency: summercms.go only. Check `git status --porcelain` on each task's paths first and stop if another session has one of them modified. Stage by explicit path; no co-author tags; planning docs and code in separate commits; README and docs in the same commit as the API they describe; `go vet ./...`, `go test -short ./...` and `go -C examples/hello test ./...` green at every commit. Nothing is pushed or tagged. + + +Existing seams: + +- modules/bonfire/command.go: `Command{Name, Description, Long string; Passthrough bool; Flags []Flag; Args []Arg; Run}` (lines 15-32), `Flag{Name, Shorthand, Description, Default string; Bare, Repeatable bool}`, `Arg{Name, Description string; Required bool}`, `Input`, `validCommandName` (bare names build, dev, new, serve, migrate), `commandUsage`, `positionalArgs`. +- modules/bonfire/root.go: `NewRoot(name, commands, out)`, `NewRootIO(name, commands, in, out, errW)` (root with SilenceUsage, SilenceErrors, DisableDefaultCmd), `wrap(c, out)` (flag registration with StringP/StringSliceP, `NoOptDefVal = "true"` for Bare). +- modules/bonfire/call.go: `Call(ctx, commands, name, args, out)` builds a fresh NewRootIO per call with empty stdin (scheduler path), `Catalog`, `findCommand`. +- modules/bonfire/output.go: `Output` interface (Println, Printf, Info, Success, Error, Warning, Table, Spinner, Progress, Ask, Confirm, Choice, Secret, io.Writer), `console` struct with `tty`, `interactive`, `color`, injectable `lookup`; `newConsole(in, out, errW, lookup, tty, interactive *bool)`; widgets.go non-TTY fallbacks (`[...] message`, decile percentage lines, tab-separated table); prompts.go line-based prompts and `Secret` reading a plain line when stdin is not a terminal. +- Existing tests that must stay green unchanged in intent: modules/bonfire/output_test.go (non-TTY output, NO_COLOR/TERM=dumb/FORCE_COLOR presence of escape codes, `TestHelpUsesSharedAdapter`), prompts_test.go, call_test.go, modules/cabana/commands_test.go:241-262 (WR-13 piped password), cmd/summer/main_test.go (help output contains each flag name and default). +- modules/diary (plan 01): `Options`, `Bootstrap`, `Setup`, `SetVerbosity`, `NewConsoleHandler`; modules/bonfire/styles.go: `ColorMode`, `Palette`, `DefaultPalette`, `DetectProfile`. +- internal/build/build.go generateMain (plan 01 shape) and `TestGenerateMainInstallsDiary`; examples/hello/hello_test.go (run(...) error today; TestHelpListsServe, TestMigrateWithoutDSNFailsLoudly, TestBuiltBinaryUnknownCommandExitsNonZero, runSummerBuild); internal/build/build_test.go `TestBuiltHelloFailsOnInvalidRequires` (combined output must still name both plugin IDs). +- internal/docsite/check_commands.go:99-125 `commandWord` (assumes only --help/-h on the root), cmd/summer/docs.go:144-165 `docsCommands`, cmd/summer/docs_test.go:418 `generatedConstructor` regex over `commands = append(commands, pkg.Ctor(app` lines. +- fang v2 source (module cache after go get): fang.go `Execute`, `WithoutVersion`, `WithoutManpage`, `WithoutCompletions`, `WithErrorHandler`, `ErrorHandler` signature; it wraps stderr in colorprofile.NewWriter before calling the handler (RESEARCH Pattern 5). +- SSU widgets to port (external repo, read-only): /media/nvme/dev/pxpx/ssu/internal/cli/tui/spinner.go, process.go, progress.go, selector.go, selector_keys.go, confirm.go, styles.go; RESEARCH "Porting SSU's spinner to bubbletea v2" for the API renames, progress WithColors cyan #00afaf to green #00af00 and SetWidth(40). +- cobra v1.10.2: a command's own helpFunc wins over its parent's; `SuggestionsFor`; `SetHelpCommand`; `SetFlagErrorFunc`. + + + + + + + Task 1: Tracer: a bare application binary prints the Artisan list with the version header and global options, -vv and -q on any command reach diary before plugins boot, and the process exits 0, 1 or 2 through bonfire.Execute + Rendering and flag plumbing sit behind NewRoot's unchanged signature (options are variadic); the generated main is rewritten by every summer build. + go.mod, go.sum, modules/bonfire/globals.go, modules/bonfire/root.go, modules/bonfire/version.go, modules/bonfire/execute.go, modules/bonfire/list.go, modules/bonfire/list_smoke_test.go, modules/bonfire/README.md, modules/diary/diary.go, modules/diary/README.md, internal/build/build.go, internal/build/build_test.go, internal/build/testdata/new/main.go.golden, examples/hello/main.go, examples/hello/hello_test.go, examples/hello/go.sum, examples/hello/plugins/greeter/go.sum, cmd/summer/main.go, cmd/summer/docs.go, internal/docsite/check_commands.go, docs/console/introduction.md, docs/console/writing-commands.md, .planning/todos/pending/bonfire-duplicate-command-names.md, .planning/todos/done/bonfire-duplicate-command-names.md + modules/bonfire/root.go, modules/bonfire/command.go, modules/bonfire/call.go, modules/bonfire/output.go, modules/bonfire/README.md, modules/diary/diary.go, internal/build/build.go, internal/build/build_test.go, examples/hello/hello_test.go, cmd/summer/main.go, cmd/summer/docs.go, internal/docsite/check_commands.go, docs/console/introduction.md ("Getting help"), docs/console/writing-commands.md (the duplicate-names paragraph), .planning/todos/pending/bonfire-duplicate-command-names.md, .planning/G15OFFICE.md, 19.8-RESEARCH.md (Pattern 4, Pattern 5, Pitfalls 1-3 and 15, Code Examples "Generated main after this phase", Probe evidence), 19.8-CONTEXT.md (D-13 preview) + Install `charm.land/fang/v2` at the decision note's version (`go get`, then `go mod tidy` once execute.go imports it, and tidy the examples/hello modules that need go.sum entries). + +globals.go: `Globals` (fields under Artifacts). `PeekGlobals(args)`: a tolerant scan that never errors and stops at `--`: `-v`, `-vv`, `-vvv` and each `--verbose` add to Verbosity (capped at 3); `-q`/`--quiet`; `-n`/`--no-interaction`; `--ansi` and `--no-ansi` set Color to ColorAlways or ColorNever, last wins; `--env X` and `--env=X`; Quiet also sets NoInteraction. `(Globals).Args()` re-serialises for forwarding (`-v`/`-vv`/`-vvv`, `-q`, `-n`, `--ansi` or `--no-ansi`, `--env=`), empty for the zero value. + +root.go: `type RootOption func(*rootConfig)`, `WithVersion(v Version)`, `OnGlobals(fn func(Globals))`; `NewRoot(name, commands, out, opts ...RootOption)` and `NewRootIO(name, commands, in, out, errW, opts ...RootOption)`. Register persistent flags on the root: `--verbose`/`-v` as a count, `--quiet`/`-q`, `--ansi`, `--no-ansi`, `--no-interaction`/`-n`, `--env` (string), with the D-13 descriptions. In the command loop: a second command with the same name, or a command named `list` or `help`, returns `fmt.Errorf("%w: %q", ErrDuplicateCommand, name)`; in wrap, a flag whose Name is verbose, quiet, ansi, no-ansi, no-interaction, env or help, or whose Shorthand is v, q, n or h, returns an error wrapping ErrReservedFlag that names the command and the flag (checked before pflag registration so it never panics). Root PersistentPreRunE: read the parsed flags into Globals (Quiet implies NoInteraction), store them on the console Output (new unexported fields; Task 3 acts on them) and call every OnGlobals hook. `bonfire.Call` passes no options, so the scheduler never changes process-wide logging (RESEARCH Pitfall 3). Root RunE renders the list (bare binary). Add the `list` command (Description "List commands"; flags `--all` Bare and `--format` default `txt`; in this task only txt, Task 2 adds json and Hidden). + +version.go: `Version`, `BuildVersion()` from debug.ReadBuildInfo: App is Main.Version with "(devel)" shown as "devel"; Framework is the version of the dep `git.golem15.com/golem15/summercms` ("(devel)" as "devel", Local true when the dep has a Replace); when the main module is the framework itself (the summer tool) Framework equals App. `(Version).Header(name)`: `SummerCMS acme v1.4.0 (framework v0.3.2)`, `(framework v0.3.2, local)` when Local, and `SummerCMS summer v0.3.2` when App and Framework are the same build. + +list.go (txt): Version header line, blank line, "Usage:" and ` [options] [arguments]` (usage name `summer` for the tool, else `bin/`), "Options:" with exactly the D-13 global option lines, "Available commands:" with top-level names (no colon, including list and help) first, then each namespace (the text before the first colon) sorted with a one-space-indented header line and its commands sorted, every name padded to the longest plus two spaces before the description. Styling: section titles in Palette.Header, command names in Palette.Success, through a colorprofile.Writer built with DetectProfile(out, globals.Color, os.Environ()). + +execute.go: constants ExitOK, ExitFailure, ExitUsage; `Execute(ctx, root, args)`: SetArgs(args); when the args before `--` hold no command word (only globals and -h/--help), render the list to root's out and return ExitOK; otherwise `fang.Execute(ctx, root, fang.WithoutVersion(), fang.WithoutManpage(), fang.WithoutCompletions(), fang.WithErrorHandler(h))` where h calls RenderError (in this task a plain block: a blank line, ` ERROR `, a blank line); map the returned error: nil to ExitOK, a UsageError (root.SetFlagErrorFunc wraps flag errors with Usage; the positional-args validator wraps count errors; messages starting with "unknown command" count too) to ExitUsage, anything else to ExitFailure. `Fail(w, name, err, g)` renders and returns ExitFailure for errors before the root exists. Usage, UsageError and RenderError are declared here or in errors.go (Task 2 completes the renderer). + +diary: `FromGlobals(g bonfire.Globals) Options` (Verbosity, Quiet, Color) and `ApplyGlobals(g bonfire.Globals)` (SetVerbosity plus the console color mode). README rows for both. + +Generated main: `func main() { os.Exit(run(os.Args[1:], os.Stdout)) }`; `func run(args []string, out io.Writer) int`: `g := bonfire.PeekGlobals(args)`, `diary.Bootstrap(os.Stderr, diary.FromGlobals(g))`, `cfg, err := compass.Open(compass.Options{Dir: "config", Env: g.Env})` with `return bonfire.Fail(os.Stderr, "", err, g)` on error (same for diary.Setup, party.Activate, app.Publish and NewRoot errors), `diary.Setup(cfg, diary.FromGlobals(g))`, unchanged command assembly lines, `root, err := bonfire.NewRoot("", commands, out, bonfire.WithVersion(bonfire.BuildVersion()), bonfire.OnGlobals(diary.ApplyGlobals))`, `return bonfire.Execute(context.Background(), root, args)`. Update `TestGenerateMainInstallsDiary` for the new order (PeekGlobals first, then Bootstrap, then compass.Open, then Setup, then backpack.New) and the goldens (`-update`); regenerate examples/hello/main.go. cmd/summer/main.go: same shape (PeekGlobals, diary.Bootstrap, NewRoot with WithVersion and OnGlobals, `os.Exit(bonfire.Execute(...))`). + +examples/hello/hello_test.go: `TestHelpListsServe` expects run to return 0 and the list to name serve, migrate, migrate:status and migrate:rollback plus "Available commands:"; `TestMigrateWithoutDSNFailsLoudly` asserts run returns 1 and that the built binary's stderr for `migrate` with an empty DSN contains `database.dsn`; `TestBuiltBinaryUnknownCommandExitsNonZero` asserts exit code 2; new `TestBuiltBinaryListsCommands`: `./bin/hello` exits 0 and stdout holds "SummerCMS hello", "Available commands:", a line equal to " greeter" and "greeter:hello"; `./bin/hello -vv list` exits 0. + +Docs checker: commandWord treats `-v`, `-vv`, `-vvv`, `--verbose`, `-q`, `--quiet`, `-n`, `--no-interaction`, `--ansi`, `--no-ansi` as value-less and `--env` as taking a value (`--env X` or `--env=X`); update its doc comment. docsCommands adds "list" and "help" to both the tool and the application lists. Keep the generated-main append lines that docs_test parses. + +Docs and README in the same commit: docs/console/introduction.md "Getting help" shows the bare binary's list, the global options table and the exit codes 0, 1 and 2; writing-commands.md replaces "names are not checked for duplicates" with the rejection rule (ErrDuplicateCommand at boot; global flag names and shorthands are reserved, ErrReservedFlag); bonfire README Features and API reference rows for every new name in this task. Commit: `feat(bonfire): global flags, Artisan list and exit codes`. + +Smoke test list_smoke_test.go `TestListTracerSmoke` (-short): NewRootIO("acme") with commands migrate, admin:create, admin:token:create, make:model and an OnGlobals hook that records Globals; Execute with no args returns 0 and the output holds the header, "Usage:", " bin/acme [options] [arguments]", the global option lines and, in order, migrate, then " admin" with its two commands, then " make"; `-vvv migrate` records Verbosity 3; `migrate -q` records Quiet and NoInteraction; `migrat` returns 2; a command whose Run fails returns 1; a duplicate name and a flag with Shorthand "v" each make NewRootIO return an error that names the command. PeekGlobals agrees with the hook for the same args. + +Close the folded todo (planning commit, alone): `git mv .planning/todos/pending/bonfire-duplicate-command-names.md .planning/todos/done/`, append one line naming the commit; move g15office task u4rtpcl1 to Live with a one-line comment (if the g15office tool is not available in this session, record that in the SUMMARY). Commit: `docs(19.8): close the duplicate command names todo`. + + go vet ./... && go test -count=1 -short ./modules/bonfire/... ./modules/diary/... ./internal/build/... ./internal/docsite/... ./cmd/summer/... && go test -count=1 -short -run '^TestListTracerSmoke$' -v ./modules/bonfire 2>&1 | grep -q -- '^--- PASS: TestListTracerSmoke' && go -C examples/hello test -count=1 -run '^(TestBuiltBinaryListsCommands|TestHelpListsServe|TestBuiltBinaryUnknownCommandExitsNonZero)$' -v . 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 3 && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check + Non-zero exit; no "--- PASS: TestListTracerSmoke" line; the count of top-level "--- PASS: Test" lines from examples/hello is not 3 (the built binary does not list, the help test fails, or an unknown command does not exit 2); or the docs checker rejects a command line or identifier. + + + - `grep -c 'func PeekGlobals(args \[\]string) Globals' modules/bonfire/globals.go` prints 1. + - `grep -c 'fang.WithoutVersion()' modules/bonfire/execute.go` prints 1 and `grep -c 'fang.WithoutManpage()' modules/bonfire/execute.go` prints 1. + - `grep -c 'bonfire.OnGlobals(diary.ApplyGlobals)' internal/build/testdata/new/main.go.golden` prints 1 and `grep -c 'os.Exit(run(os.Args\[1:\], os.Stdout))' examples/hello/main.go` prints 1. + - `grep -c 'compass.Options{Dir: "config", Env: g.Env}' internal/build/testdata/new/main.go.golden` prints 1. + - `grep -c 'ErrDuplicateCommand' modules/bonfire/README.md` prints at least 1 and `grep -c 'ErrDuplicateCommand' docs/console/writing-commands.md` prints at least 1. + - `test -f .planning/todos/done/bonfire-duplicate-command-names.md && test ! -f .planning/todos/pending/bonfire-duplicate-command-names.md` exits 0. + - `grep -cE '"--env"|--no-interaction' internal/docsite/check_commands.go` prints at least 1. + + A user who runs an application binary or summer with no arguments sees the grouped Artisan list, every command takes the global flags with diary following them from the first log line, and scripts get exit code 2 for usage mistakes. + + + + Task 2: help <command> and --help print the Artisan layout with defaults, long help and examples; Hidden, list --all and list --format=json; CheckDocs; errors render as a styled block with a Try: hint and the -v chain + D-15 and D-16: Command.Examples, Command.Hidden and the JSON listing's field names become API that plugins and docs checks rely on. + modules/bonfire/command.go, modules/bonfire/root.go, modules/bonfire/help.go, modules/bonfire/list.go, modules/bonfire/docs.go, modules/bonfire/errors.go, modules/bonfire/execute.go, modules/bonfire/help_smoke_test.go, modules/bonfire/README.md, docs/console/writing-commands.md, docs/console/introduction.md + modules/bonfire/command.go, modules/bonfire/root.go, modules/bonfire/list.go, modules/bonfire/execute.go (Task 1), modules/bonfire/output_test.go (TestHelpUsesSharedAdapter), cmd/summer/main_test.go (help expectations :20-58), modules/sunscreen/sunscreen.go (Scrub), docs/console/writing-commands.md, docs/console/introduction.md, 19.8-CONTEXT.md (D-14 preview, D-16, D-17, D-18 preview), 19.8-RESEARCH.md (Pattern 5 JSON schema and CheckDocs, Pitfall 13, Code Examples "Error with hint") + command.go (D-15): append `Examples []Example` and `Hidden bool` to Command (after Run, with doc comments) and add `type Example struct { Command string; Description string }`: Command is the command line without the binary, Description a short sentence shown as a `#` comment above it. wrap maps Hidden to cobra's Hidden and sets `cmd.SetHelpFunc` on every subcommand to the Artisan help renderer (RESEARCH: cobra prefers a command's own help func); root help func renders the list. Replace cobra's help command with `help [command]` via root.SetHelpCommand: no argument prints the list, a name prints that command's help, an unknown name is a Usage error whose Try comes from root.SuggestionsFor. + +help.go (D-14): sections in order, each title in Palette.Header: "Description:" with the Description; "Usage:" with ` [options]` (append `[--] ` only for Passthrough commands); "Arguments:" (name padded, description) when there are Args; "Options:" one line per flag: `-x, --name=NAME` for value flags (` --name=NAME` without shorthand, NAME the upper-cased flag name with dashes as underscores), `--name` for Bare flags, description, then `[default: "value"]` when Default is non-empty (unquoted when it parses as an integer), and "(multiple values allowed)" for Repeatable; "Global options:" the compact global flag lines; "Help:" with Long when set, indented two spaces and wrapped as written; "Examples:" each example as ` # ` then ` `, a blank line between examples. Output through the same colorprofile writer as the list. + +list.go: Hidden commands are skipped unless `--all`; `--format=json` writes `Describe(rootName, commands, version)` as indented JSON; any other format is a Usage error. `Describe` builds `Listing` with JSON keys application{name, version, framework}, global_options, commands[{name, description, long, usage, hidden, passthrough, arguments[{name, description, required}], options[{name, shorthand, description, default, bare, repeatable}], examples[{command, description}]}] covering every command including hidden ones, list and help. + +docs.go (D-17): `DocProblem` with `String()` (": missing description, examples"), `CheckDocs(commands)` checking Description, Long, at least one Example, every Arg.Description and every Flag.Description; results sorted by command name. It only reports; callers decide to fail (framework, plan 3) or log (plugins). + +errors.go (D-18): `HintError` (Error returns Err.Error(), Unwrap returns Err), `WithHint(err, explanation, try)` (nil stays nil), `ErrReported` and `Reported(err)` (an error matching both ErrReported and err), `UsageError` and `Usage(err)`. `RenderError(w, name, err, g)`: nothing for ErrReported; otherwise, through a colorprofile writer with DetectProfile(w, g.Color, os.Environ()), a blank line, ` ` + an ` ERROR ` badge (Palette.ErrorBold, reversed) + two spaces + the error message (every line indented, text passed through sunscreen.Scrub); when the chain holds a HintError, a blank line and its explanation, then " Try:" and ` ` with `{bin}` replaced by the usage name; when the error is cobra's unknown command with suggestions, the message becomes `Command "" is not defined.` and Try lists ` `; for other usage errors Try is ` help `; at Verbosity 0 a final " Run with -v for details." line, at Verbosity 1 or more a " Caused by:" list of each wrapped error's message (errors.Unwrap walk, errors.Join members expanded), scrubbed. execute.go uses RenderError from fang's handler and in Fail. Existing callers that only check `err != nil` are unaffected. + +README (same commit): Features (Artisan help and list, Examples and Hidden, JSON listing, CheckDocs, error block with hints and exit codes), API reference rows for every name added in this task. docs/console/writing-commands.md: sections "Examples and hidden commands" (the Example shape, rendering with bin/ or summer), "Errors and hints" (WithHint with the {bin} placeholder, Reported for commands that already printed their problems, exit codes), "Documentation coverage" (CheckDocs; the framework test fails, plugin repos log). docs/console/introduction.md "Getting help": `summer help migrate`, `bin/acme migrate --help`, `bin/acme list --all`, `bin/acme list --format=json`. Commit: `feat(bonfire): Artisan help, examples, JSON listing and error hints`. + +Smoke tests help_smoke_test.go (-short): `TestHelpSmoke`: a command with one required Arg, a value flag with Default "*", a value flag with Default "90", a Bare flag, a Repeatable flag, Long and two Examples renders the sections in order with `[default: "*"]`, `[default: 90]`, "(multiple values allowed)" and `bin/acme` prefixes; `help ` output equals ` --help` output. `TestListJSONSmoke`: `list --format=json` decodes into Listing, includes a Hidden command with hidden true while plain `list` omits it and `list --all` shows it. `TestErrorRenderSmoke`: WithHint renders the explanation and the Try line with {bin} replaced; Verbosity 0 shows "Run with -v for details." and Verbosity 1 shows "Caused by:"; Reported renders nothing and Execute returns 1; an unknown command returns 2 with a Try naming the suggestion; a "Bearer abc..." string in an error is scrubbed. Existing TestHelpUsesSharedAdapter and cmd/summer help tests stay green. + + go vet ./... && go test -count=1 -short ./modules/bonfire/... ./cmd/summer/... ./internal/build/... && go test -count=1 -short -run '^(TestHelpSmoke|TestListJSONSmoke|TestErrorRenderSmoke|TestHelpUsesSharedAdapter)$' -v ./modules/bonfire 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 4 && go -C examples/hello test -count=1 ./... && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check + Non-zero exit, or the count of top-level "--- PASS: Test" lines is not 4 (a help section, the JSON listing, the error block or the old help test failed or did not run), or examples/hello or the docs checker fail. + + + - `grep -cE '^\s+Examples +\[\]Example' modules/bonfire/command.go` prints 1 and `grep -cE '^\s+Hidden +bool' modules/bonfire/command.go` prints 1. + - `S=$(awk '/^type Command struct/,/^}/' modules/bonfire/command.go) && R=$(printf '%s\n' "$S" | grep -nE '^\s+Run\s' | cut -d: -f1) && E=$(printf '%s\n' "$S" | grep -nE '^\s+Examples\s' | cut -d: -f1) && H=$(printf '%s\n' "$S" | grep -nE '^\s+Hidden\s' | cut -d: -f1) && test "$R" -lt "$E" && test "$R" -lt "$H"` exits 0 (Examples and Hidden are appended after Run, additive). + - `grep -c 'func CheckDocs(commands \[\]Command) \[\]DocProblem' modules/bonfire/docs.go` prints 1 and `grep -c 'func WithHint(err error, explanation, try string) error' modules/bonfire/errors.go` prints 1. + - `grep -c 'sunscreen.Scrub' modules/bonfire/errors.go` prints at least 1. + - `grep -c '"global_options"' modules/bonfire/list.go` prints 1. + - `grep -cE 'WithHint|CheckDocs|Reported' docs/console/writing-commands.md` prints at least 3. + + Any command explains itself in the Artisan layout with its defaults and examples, scripts and agents can read the whole catalog as JSON, and failures end in a readable block that says what to type next. + + + + Task 3: On a terminal, spinners, the multi-row process view, the gradient progress bar, tables and the arrow-key selector run on charm v2 (ported from SSU); off a terminal, under -q, --no-ansi or -n every widget prints plain lines and prompts never block + D-01 and D-02: every command's terminal output and the widget behaviour depend on the ported models and the charm runtime. + go.mod, go.sum, modules/bonfire/styles.go, modules/bonfire/output.go, modules/bonfire/widgets.go, modules/bonfire/prompts.go, modules/bonfire/live.go, modules/bonfire/selector.go, modules/bonfire/helpers.go, modules/bonfire/widgets_smoke_test.go, modules/bonfire/README.md, modules/diary/console.go, modules/diary/README.md, docs/console/writing-commands.md, docs/services/logging.md, examples/hello/go.sum, examples/hello/plugins/greeter/go.sum + modules/bonfire/output.go, modules/bonfire/widgets.go, modules/bonfire/prompts.go, modules/bonfire/styles.go, modules/bonfire/output_test.go, modules/bonfire/prompts_test.go, modules/cabana/commands_test.go (:241-262 WR-13), modules/diary/console.go, /media/nvme/dev/pxpx/ssu/internal/cli/tui/spinner.go, /media/nvme/dev/pxpx/ssu/internal/cli/tui/process.go, /media/nvme/dev/pxpx/ssu/internal/cli/tui/progress.go, /media/nvme/dev/pxpx/ssu/internal/cli/tui/selector.go, /media/nvme/dev/pxpx/ssu/internal/cli/tui/selector_keys.go, /media/nvme/dev/pxpx/ssu/internal/cli/tui/confirm.go, 19.8-RESEARCH.md (Anti-Patterns, Don't Hand-Roll, Porting SSU's spinner, Pitfalls 7, 10 and 12, Testing techniques), 19.8-CONTEXT.md (D-01, D-03, D-27, D-28, D-30) + Install `charm.land/bubbletea/v2`, `charm.land/bubbles/v2` and `charm.land/huh/v2` at the decision note's versions; tidy the root and examples/hello modules. + +Console policy (output.go): the console keeps its fields and gains quiet, noInteraction, verbosity and color mode from Task 1's PersistentPreRunE. live = tty and not quiet and color mode not ColorNever and not noInteraction (D-03). interactive = stdin and stdout are terminals and not noInteraction (quiet implies noInteraction). Styling moves from the raw ANSI constants to DefaultPalette through a colorprofile.Writer whose profile comes from DetectProfile (NO_COLOR, TERM=dumb and FORCE_COLOR keep their meaning; the symbols ✓ ✗ ⚠ ℹ stay). Info, Success and Warning print nothing when quiet; Error always prints to stderr; Println, Printf, Write and Table always print (data, D-27). Off a terminal the bytes are today's (output_test.go and prompts_test.go must pass unchanged). + +live.go: port SSU's SpinnerModel, ProcessModel and ProgressModel to bubbletea v2 as unexported models (View returns tea.View via tea.NewView, key messages are tea.KeyPressMsg, progress.New(progress.WithColors(lipgloss.Color("#00afaf"), lipgloss.Color("#00af00")), progress.WithoutPercentage()) with SetWidth(40), spinner.MiniDot with Palette.Current) without the git and engine coupling. A helper runs a model with `tea.NewProgram(m, tea.WithInput(nil), tea.WithOutput(out), tea.WithoutSignalHandler(), tea.WithContext(ctx))`, keeps a package-level pointer to the running program under a mutex for PrintAbove, and clears it before Quit returns (never Println after quit). Spinner: live runs the spinner model while fn runs in a goroutine and ends with "✓ message" or "✗ message"; not live keeps `[...] message`; quiet runs fn silently. Progress: live shows the bar with done/total and the current item; the value passed to fn implements Advance plus the optional ItemProgress methods; not live keeps the decile percentage lines; quiet prints nothing. Table: on a terminal lipgloss/v2/table with lipgloss.NormalBorder() and Palette.Header for the header row; off a terminal the existing tab-separated output. + +selector.go: a single-select port of SSU's SelectorModel and its key map (up/down and k/j move, enter selects, esc or ctrl+c cancels) plus SSU's confirmation screen (selected item, enter to confirm, esc to go back); no multi-select, filter or detail pane (D-30). + +prompts.go: interactive Ask uses huh.NewInput (default as placeholder and returned on empty input), Confirm huh.NewConfirm, Secret huh.NewInput().EchoMode(huh.EchoModePassword), Choice the selector; forms run with WithInput(c.in) and WithOutput(c.out); huh.ErrUserAborted and a cancelled selector return ErrAborted. Not interactive and not noInteraction: today's line-based prompts unchanged (piped stdin, empty stdin takes defaults, WR-13). noInteraction: Ask returns def, Confirm returns def, Choice returns choices[def], Secret reads one line when stdin is not a terminal and otherwise returns `WithHint(errors.New("bonfire: a secret is needed and -n turned prompts off"), "This command reads the value from a prompt or from stdin.", "printf '%s\n' \"$VALUE\" | {bin} -n")`. + +helpers.go: GlobalsOf, VerbosityOf, IsQuiet and Interactive read an unexported optional interface the console implements (zero values for other Output implementations); ConfirmProceed returns true without asking when Interactive is false and otherwise asks Confirm(question, false) (D-28); Process runs items one at a time with the process model on a live console (per-row pending, running, done or failed with the error) and one line per item otherwise ("✓ item" or "✗ item: error"; quiet prints failures only through Error), returning errors.Join of the failures; Current and Failed call the ItemProgress methods when the Progress has them; PrintAbove prints a line above the running program and reports whether it did. + +diary console.go: when the console channel renders styled lines on a terminal it first offers the formatted line to bonfire.PrintAbove and writes directly only when that returns false, so log lines from other goroutines never tear a live view. + +README and docs in the same commit: bonfire README Features (charm-backed widgets; a degradation table with rows terminal, not a terminal, -q, --no-ansi, -n and columns animation, decoration, data, prompts), API reference rows for the helpers, ItemProgress and ErrAborted, Dependencies list the charm modules; docs/console/writing-commands.md Output section (the same table, -q keeps data, ConfirmProceed for destructive commands, Process and Current for bulk loops); docs/services/logging.md one paragraph on log lines printing above live views. Commit: `feat(bonfire): SSU widgets on charm v2 behind the Output interface`. + +Smoke test widgets_smoke_test.go `TestWidgetMatrixSmoke` (-short, no terminal): through newConsole with injected tty and interactive flags: quiet suppresses Info, Success and Warning but keeps Println, Table and Error; noInteraction makes Ask, Confirm and Choice return defaults without reading stdin and Secret with a terminal-like stdin returns an error whose chain holds a HintError; ConfirmProceed on a non-interactive console returns true without reading; the spinner, progress, process and selector models produce non-empty frames from Update and View called directly (window size 80x24), and the selector returns the item after enter on the confirmation screen. + + go vet ./... && go test -count=1 -short ./modules/bonfire/... ./modules/diary/... ./modules/cabana/... && go test -count=1 -race -short ./modules/bonfire/... ./modules/diary/... && go test -count=1 -short -run '^(TestWidgetMatrixSmoke|TestNonTTYSpinner|TestNonTTYProgress|TestNonTTYTable|TestColorPolicy|TestSecretReadsPlainStdinLine|TestClosedStdinDoesNotHang)$' -v ./modules/bonfire 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 7 && go -C examples/hello test -count=1 ./... && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check + Non-zero exit (including a data race reported by -race), or the count of top-level "--- PASS: Test" lines is not 7 (the matrix smoke or one of the unchanged non-TTY, color or piped-secret tests failed or did not run), or examples/hello, the cabana piped-password test or the docs checker fail. + + + - `grep -c 'tea.WithInput(nil)' modules/bonfire/live.go` prints at least 1 and `grep -c 'tea.WithoutSignalHandler()' modules/bonfire/live.go` prints at least 1. + - `go list -deps ./modules/bonfire | grep -cE '^charm.land/(bubbletea|bubbles|huh|fang|lipgloss)/v2$'` prints 5. + - `grep -c 'huh.EchoModePassword' modules/bonfire/prompts.go` prints 1 and `grep -cE 'engine\.|git\.Submodule' modules/bonfire/live.go modules/bonfire/selector.go | awk -F: '{s+=$2} END {print s}'` prints 0. + - `grep -c 'bonfire.PrintAbove(' modules/diary/console.go` prints at least 1. + - `C=$(git log --oneline cdddaa3..HEAD -- modules/bonfire/output_test.go modules/bonfire/prompts_test.go) && test -z "$C"` exits 0 (the existing non-TTY and prompt tests were not edited in this phase). + - `grep -c 'ConfirmProceed' docs/console/writing-commands.md` prints at least 1. + + On a terminal commands look like SSU, off a terminal or with -q, --no-ansi or -n they print the same plain lines as before and never wait for input, and plugins needed no change to get any of it. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| argv to bonfire and the generated main | global flags are read before config loads and before plugins boot | +| commands to the terminal | error text, hints and data printed to stdout and stderr, possibly captured by CI logs | +| operator stdin to prompts | passwords and confirmations, piped in scripts or typed on a terminal | +| plugins to the command root | plugin commands are registered next to framework commands | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.8-07 | Information disclosure | RenderError (errors.go) | medium | mitigate | every rendered message and chain entry passes through sunscreen.Scrub; the wrapped chain prints only at -v and only to stderr; TestErrorRenderSmoke checks a Bearer token is scrubbed | +| T-19.8-08 | Denial of service | prompts and ConfirmProceed | high | mitigate | huh and the selector run only when stdin and stdout are terminals and -n/-q are off; Call roots have empty stdin; ConfirmProceed returns true off a terminal (D-28); TestWidgetMatrixSmoke and the unchanged piped-stdin tests | +| T-19.8-09 | Information disclosure | -q data path | medium | mitigate | tokens and keys print only through Println/Printf/Write or --json, never through diary; -q suppresses decoration only (D-27), so a script's data is neither lost nor duplicated into logs | +| T-19.8-10 | Elevation of privilege | NewRoot command registration | medium | mitigate | duplicate names (and list or help) make NewRoot fail at boot naming the command, so a plugin cannot shadow a framework command such as migrate; reserved global flags are rejected instead of panicking | +| T-19.8-11 | Denial of service | bubbletea programs (live.go) | medium | mitigate | tea.WithInput(nil) keeps stdin out of raw mode, tea.WithoutSignalHandler leaves Ctrl+C to the command's signal.NotifyContext, tea.WithContext stops the view with the command | +| T-19.8-12 | Tampering | --env flag | low | mitigate | the value goes only to compass.Options.Env, whose sanitizer rejects path separators and dot-dot | +| T-19.8-SC | Tampering | Go module installs (fang, bubbletea, bubbles, huh) | high | mitigate | versions pinned by the plan 01 decision note and the RESEARCH legitimacy audit; go.sum verified; only modules/bonfire imports them (acceptance criterion counts the module set) | + + + +- Task 1: TestListTracerSmoke; examples/hello lists, helps and exits 2 on an unknown command; docs checker accepts global flags, list and help. +- Task 2: TestHelpSmoke, TestListJSONSmoke, TestErrorRenderSmoke; existing help tests unchanged. +- Task 3: TestWidgetMatrixSmoke plus the unchanged non-TTY, color and piped-secret tests; -race on bonfire and diary. +- Feedback sampling: after each commit `go test -short ./modules/bonfire/... ./modules/diary/... ./internal/build/... ./internal/docsite/... ./cmd/summer/...`; after the plan, `go vet ./... && go test -short ./... && go -C examples/hello test ./...`. + + + +- D-13, D-14, D-15, D-16: list, help, Examples, Hidden and the JSON listing. +- D-17: CheckDocs helper ready for plan 3's failing framework test and warn-only plugin tests. +- D-18: styled error block with hints and exit codes 1 and 2. +- D-01, D-02, D-03, D-27, D-28, D-30: SSU widgets on charm v2 with clean degradation; Output unchanged. +- SC-2: global flags on every command drive diary. +- Folded todo closed. + + + +Create `.planning/phases/19.8-console-commands-overhaul/19.8-02-SUMMARY.md` when done, with the commits in order, the charm versions installed, the final exported names (plan 3 uses WithHint, Reported, ConfirmProceed, Process, GlobalsOf, Examples), the g15office result for u4rtpcl1, and any deviation from this plan's names. + diff --git a/.planning/phases/19.8-console-commands-overhaul/19.8-03-PLAN.md b/.planning/phases/19.8-console-commands-overhaul/19.8-03-PLAN.md new file mode 100644 index 00000000..957792d2 --- /dev/null +++ b/.planning/phases/19.8-console-commands-overhaul/19.8-03-PLAN.md @@ -0,0 +1,710 @@ +--- +phase: 19.8-console-commands-overhaul +plan: 03 +type: execute +wave: 3 +depends_on: ["19.8-01", "19.8-02"] +files_modified: + - modules/surf/serve.go + - modules/surf/summary.go + - modules/surf/accesslog.go + - modules/surf/routelist_command.go + - modules/surf/serve_summary_smoke_test.go + - modules/surf/serve_lifecycle_test.go + - modules/surf/README.md + - modules/lagoon/migrations.go + - modules/lagoon/commands.go + - modules/lagoon/keygen.go + - modules/lagoon/connection.go + - modules/lagoon/README.md + - modules/conga/worker.go + - modules/conga/commands.go + - modules/conga/README.md + - modules/cabana/commands.go + - modules/cabana/token_commands.go + - modules/cabana/README.md + - modules/lighthouse/centrifugo/commands.go + - modules/lighthouse/README.md + - modules/flare/commands.go + - modules/flare/README.md + - modules/bonfire/errors.go + - modules/bonfire/README.md + - modules/diary/diary.go + - modules/diary/console.go + - modules/diary/README.md + - internal/dev/watch.go + - internal/dev/watch_test.go + - internal/build/manifest.go + - internal/build/artifact.go + - internal/build/scaffold.go + - cmd/summer/main.go + - cmd/summer/runtime.go + - cmd/summer/parity.go + - cmd/summer/docs.go + - cmd/summer/commands_documented_test.go + - examples/hello/plugins/greeter/plugin.go + - examples/hello/hello_test.go + - docs/examples/blog/console/publish.go + - docs/console/introduction.md + - docs/console/setup-and-maintenance.md + - docs/console/scaffolding.md + - docs/console/utilities.md + - docs/console/writing-commands.md + - docs/services/logging.md + - docs/setup/upgrading.md + - ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json + - ../sm-summercmsio-app/vue-summercmsio-app + - ../sm-demo-app/plugins/demo/roadmap/curated.go + - ../sm-demo-app/plugins/demo/content/ROADMAP.md + - ../sm-paymentgateway-plugin/console/pg_cancel_created_payments.go + - ../sm-paymentgateway-plugin/console/pg_cancel_sent_payments.go + - ../sm-paymentgateway-plugin/console/pg_cancel_placed_orders.go + - ../sm-paymentgateway-plugin/console/pg_finish_orders.go + - ../sm-paymentgateway-plugin/console/pg_update_currencies.go + - ../sm-paymentgateway-plugin/console/import_winter.go + - ../sm-paymentgateway-plugin/admin_settings.go + - ../sm-paymentgateway-plugin/api/actions.go + - ../sm-paymentgateway-plugin/api/errors.go + - ../sm-paymentgateway-plugin/boot.go + - ../sm-paymentgateway-plugin/controllers/orders.go + - ../sm-paymentgateway-plugin/currency/fixer.go + - ../sm-paymentgateway-plugin/events/mailers.go + - ../sm-paymentgateway-plugin/orders/bridge.go + - ../sm-paymentgateway-plugin/orders/service.go + - ../sm-paymentgateway-plugin/payments/service.go + - ../sm-paymentgateway-plugin/commands_documented_test.go + - ../sm-paymentgateway-plugin/README.md + - ../sm-paymentgateway-plugin/go.mod + - ../sm-paymentgateway-plugin/go.sum + - ../sm-pgstripe-plugin/console/import_winter.go + - ../sm-pgstripe-plugin/operator.go + - ../sm-pgstripe-plugin/subscriptions/gateway.go + - ../sm-pgstripe-plugin/subscriptions/plugin.go + - ../sm-pgstripe-plugin/commands_documented_test.go + - ../sm-pgstripe-plugin/README.md + - ../sm-pgstripe-plugin/go.mod + - ../sm-pgstripe-plugin/go.sum + - ../sm-oauthserver-plugin/console/prune.go + - ../sm-oauthserver-plugin/console/rotate_keys.go + - ../sm-oauthserver-plugin/console/import_winter.go + - ../sm-oauthserver-plugin/classes/branding.go + - ../sm-oauthserver-plugin/classes/claims.go + - ../sm-oauthserver-plugin/classes/host.go + - ../sm-oauthserver-plugin/commands_documented_test.go + - ../sm-oauthserver-plugin/README.md + - ../sm-oauthserver-plugin/go.mod + - ../sm-oauthserver-plugin/go.sum + - ../sm-journal-plugin/console/export_posts.go + - ../sm-journal-plugin/console/import_posts.go + - ../sm-journal-plugin/plugin.go + - ../sm-journal-plugin/search.go + - ../sm-journal-plugin/controllers/api/authors.go + - ../sm-journal-plugin/controllers/api/posts_search.go + - ../sm-journal-plugin/commands_documented_test.go + - ../sm-journal-plugin/README.md + - ../sm-journal-plugin/go.mod + - ../sm-journal-plugin/go.sum + - ../sm-sitemap-plugin/console/import.go + - ../sm-sitemap-plugin/console/import_winter.go + - ../sm-sitemap-plugin/classes/manifest.go + - ../sm-sitemap-plugin/routes.go + - ../sm-sitemap-plugin/commands_documented_test.go + - ../sm-sitemap-plugin/README.md + - ../sm-sitemap-plugin/go.mod + - ../sm-sitemap-plugin/go.sum + - ../sm-faq-plugin/console/import_winter.go + - ../sm-faq-plugin/controllers/api/faqs.go + - ../sm-faq-plugin/commands_documented_test.go + - ../sm-faq-plugin/README.md + - ../sm-faq-plugin/go.mod + - ../sm-faq-plugin/go.sum + - ../sm-menu-plugin/console/import_winter.go + - ../sm-menu-plugin/classes/tree.go + - ../sm-menu-plugin/controllers/api/menus.go + - ../sm-menu-plugin/commands_documented_test.go + - ../sm-menu-plugin/README.md + - ../sm-menu-plugin/go.mod + - ../sm-menu-plugin/go.sum + - ../sm-chat-plugin/console/rewrite_context_types.go + - ../sm-chat-plugin/api/errors.go + - ../sm-chat-plugin/classes/service.go + - ../sm-chat-plugin/jobs/maintain_thread_summary.go + - ../sm-chat-plugin/commands_documented_test.go + - ../sm-chat-plugin/README.md + - ../sm-chat-plugin/go.mod + - ../sm-chat-plugin/go.sum + - ../sm-chatchannels-plugin/console/rewrite_context_types.go + - ../sm-chatchannels-plugin/api/errors.go + - ../sm-chatchannels-plugin/boot.go + - ../sm-chatchannels-plugin/classes/channel_service.go + - ../sm-chatchannels-plugin/classes/search_indexer.go + - ../sm-chatchannels-plugin/commands_documented_test.go + - ../sm-chatchannels-plugin/README.md + - ../sm-chatchannels-plugin/go.mod + - ../sm-chatchannels-plugin/go.sum + - ../sm-chatvideo-plugin/console/reconcile.go + - ../sm-chatvideo-plugin/api/errors.go + - ../sm-chatvideo-plugin/classes/notifier.go + - ../sm-chatvideo-plugin/classes/user_deletion.go + - ../sm-chatvideo-plugin/commands_documented_test.go + - ../sm-chatvideo-plugin/README.md + - ../sm-chatvideo-plugin/go.mod + - ../sm-chatvideo-plugin/go.sum + - ../sm-forum-plugin/console/rewrite_context_types.go + - ../sm-forum-plugin/api/errors.go + - ../sm-forum-plugin/boot.go + - ../sm-forum-plugin/classes/forum_service.go + - ../sm-forum-plugin/commands_documented_test.go + - ../sm-forum-plugin/README.md + - ../sm-forum-plugin/go.mod + - ../sm-forum-plugin/go.sum + - ../sm-herald-plugin/console/vapid_generate.go + - ../sm-herald-plugin/api/errors.go + - ../sm-herald-plugin/boot.go + - ../sm-herald-plugin/classes/sender.go + - ../sm-herald-plugin/commands_documented_test.go + - ../sm-herald-plugin/README.md + - ../sm-herald-plugin/go.mod + - ../sm-herald-plugin/go.sum + - ../fonoteka.go/plugins/golem15/golem/console/import_settings.go + - ../fonoteka.go/plugins/golem15/golem/commands_documented_test.go + - ../fonoteka.go/plugins/golem15/golem/README.md + - ../fonoteka.go/plugins/golem15/golem/go.mod + - ../fonoteka.go/plugins/golem15/golem/go.sum + - ../fonoteka.go/plugins/golem15/feedback/console/import_settings.go + - ../fonoteka.go/plugins/golem15/feedback/controllers/api/feedback_api_controller.go + - ../fonoteka.go/plugins/golem15/feedback/jobs.go + - ../fonoteka.go/plugins/golem15/feedback/commands_documented_test.go + - ../fonoteka.go/plugins/golem15/feedback/README.md + - ../fonoteka.go/plugins/golem15/feedback/go.mod + - ../fonoteka.go/plugins/golem15/feedback/go.sum + - ../fonoteka.go/plugins/golem15/user/console/api_tokens.go + - ../fonoteka.go/plugins/golem15/user/console/backfill_handles.go + - ../fonoteka.go/plugins/golem15/user/console/require_password_change.go + - ../fonoteka.go/plugins/golem15/user/classes/admin_actions.go + - ../fonoteka.go/plugins/golem15/user/classes/authorizer.go + - ../fonoteka.go/plugins/golem15/user/classes/avatar.go + - ../fonoteka.go/plugins/golem15/user/classes/session_user.go + - ../fonoteka.go/plugins/golem15/user/controllers/api_controller.go + - ../fonoteka.go/plugins/golem15/user/controllers/registration.go + - ../fonoteka.go/plugins/golem15/user/plugin.go + - ../fonoteka.go/plugins/golem15/user/commands_documented_test.go + - ../fonoteka.go/plugins/golem15/user/README.md + - ../fonoteka.go/plugins/golem15/user/go.mod + - ../fonoteka.go/plugins/golem15/user/go.sum + - ../sm-demo-app/plugins/golem15/user + - ../sm-demo-app/plugins/golem15/journal + - ../sm-demo-app/plugins/golem15/sitemap + - ../sm-demo-app/plugins/golem15/menu + - ../sm-demo-app/plugins/golem15/faq + - ../sm-demo-app/plugins/golem15/paymentgateway + - ../sm-demo-app/plugins/golem15/pgstripe + - ../sm-demo-app/plugins/golem15/oauthserver + - ../sm-demo-app/plugins/golem15/golem + - ../sm-demo-app/plugins/golem15/feedback + - ../sm-demo-app/main.go + - ../sm-bm-app/plugins/golem15/user + - ../sm-bm-app/main.go + - ../sm-grzybyfunkcjonalne-app/plugins/golem15/user + - ../sm-grzybyfunkcjonalne-app/plugins/golem15/journal + - ../sm-grzybyfunkcjonalne-app/plugins/golem15/sitemap + - ../sm-grzybyfunkcjonalne-app/plugins/golem15/menu + - ../sm-grzybyfunkcjonalne-app/plugins/golem15/faq + - ../sm-grzybyfunkcjonalne-app/main.go + - ../sm-summercmsio-app/plugins/golem15/user + - ../sm-summercmsio-app/plugins/golem15/journal + - ../sm-summercmsio-app/main.go + - ../fonoteka.go/plugins/golem15/user + - ../fonoteka.go/plugins/golem15/golem + - ../fonoteka.go/plugins/golem15/feedback + - ../fonoteka.go/main.go + - .planning/todos/pending/user-plugin-console-sweep.md + - .planning/G15OFFICE.md +autonomous: false +requirements: [SC-3, SC-4, SC-5, D-04, D-05, D-06, D-07, D-11, D-17, D-18, D-19, D-20, D-21, D-22, D-23, D-24, D-25, D-26, D-27, D-28, D-29] +user_setup: [] + +estimate: + tokens: 320000 + raw_tokens: 320000 + tasks: 8 + confidence: low + +must_haves: + truths: + - "D-04, D-05, D-06: at default verbosity serve prints a compact aligned startup block on a terminal (app and framework version, environment, URL and admin URL, database host and migration state 'up to date' or 'N pending', plugin count and names, queue, scheduler, mail, cache, realtime and search drivers) followed by INFO logs restyled by diary; -v adds one line per plugin; -q shows only warnings and errors; off a terminal the same facts arrive as an INFO record 'startup summary' and the record 'listening on ' keeps its exact text for the applications' smoke scripts." + - "D-07, D-29: on Ctrl+C or SIGTERM, serve, queue:work and summer dev print a short report (draining, requests drained or aborted, worker job counts done, failed and handed back to the queue, uptime, drain errors) and exit 0; a drain error exits 1." + - "D-11, D-23: every request produces one INFO record 'http request' from component http with method, the matched route pattern (never the raw path, query or headers), status, duration_ms and request_id (X-Request-Id, at most 128 bytes); it is on by default, components.http: warn or http.access_log: false silences it." + - "D-25: summer dev starts bin/ serve with the global flags it was given and restarts it on rebuild." + - "D-17, D-21, D-22: every command in summercms.go (modules, cmd/summer, internal, examples/hello's greeter and docs/examples/blog) has a description, long help and examples, and cmd/summer's TestCommandsDocumented fails when bonfire.CheckDocs reports any problem; each command's Artisan checklist row in this plan is implemented as recorded (added --force, --pretend and --json flags, progress, result lines), and declined rows say why." + - "D-18, D-19: known failures end in a hint block: running an application binary from a directory without config/ below an application root suggests 'cd ; bin/ '; an empty DSN, an unreachable database, pending migrations, a missing app.key or uploads bucket each name the command that fixes them; summer make:* outside an application suggests 'cd ; summer make:...' (folded todo, messages only); flare and docs commands that already printed their problems return bonfire.Reported." + - "D-27, D-28: key:generate, admin:token:create and herald:vapid-generate still print their data under -q; migrate, migrate:rollback, queue:clear and deferred:purge ask for confirmation only in production on an interactive terminal, --force skips it, and runs off a terminal (deploys over SSH, the scheduler) proceed as today; existing --force meanings in the importers and pg:update-currencies are unchanged." + - "D-20, D-24, D-26: no framework tag or push happens before the recorded go-ahead; after it the framework is tagged and pushed with a same-day changelog post, then each shared plugin (the D-20 list plus sm-golem-plugin and sm-feedback-plugin) gets exactly one commit (description, long help, examples, named loggers, output polish, a warn-only TestCommandsDocumented, require of the new framework tag) pushed before its applications get separate pointer-bump commits; no command name or flag is removed or renamed. When Phase 19.5 still holds the user plugin, sm-user-plugin is deferred to .planning/todos/pending/user-plugin-console-sweep.md with a g15office task, and the SUMMARY and verification report D-20 as partial." + - "SC-5: module READMEs, docs/console pages, docs/services/logging.md, docs/setup/upgrading.md, the summercms.io landing copy and the roadmap deck in sm-demo-app describe the new console." + artifacts: + - path: "modules/surf/summary.go" + provides: "startup summary and shutdown report builders, request counters" + contains: "listening on " + - path: "modules/lagoon/migrations.go" + provides: "PendingMigrations" + contains: "func PendingMigrations(" + - path: "modules/conga/worker.go" + provides: "Worker.Stats for the shutdown report" + contains: "func (w *Worker) Stats() WorkerStats" + - path: "cmd/summer/commands_documented_test.go" + provides: "framework documentation coverage gate (fails)" + contains: "CheckDocs" + - path: "internal/dev/watch.go" + provides: "summer dev runs serve and reports on shutdown" + contains: "\"serve\"" + key_links: + - from: "modules/surf/accesslog.go" + to: "modules/diary/diary.go" + via: "request log is the http component" + pattern: "diary.For\\(\"http\"\\)" + - from: "modules/surf/serve.go" + to: "modules/lagoon/migrations.go" + via: "startup summary reports pending migrations" + pattern: "PendingMigrations\\(" + - from: "cmd/summer/commands_documented_test.go" + to: "modules/bonfire/docs.go" + via: "framework commands must pass CheckDocs" + pattern: "bonfire.CheckDocs\\(" + prohibitions: + - statement: "No command name or existing flag in summercms.go or any shared plugin is removed, renamed or given a different meaning; new flags are additive." + status: unresolved + verification: judgment + - statement: "Nothing is pushed, tagged, posted to the changelog or deployed before the Task 6 reply allows it, and nothing is ever force-pushed or deployed." + status: unresolved + verification: judgment + - statement: "The startup summary, hints and logs never print a DSN password, an app key, a token or a secret." + status: unresolved + verification: judgment + - statement: "Framework code, READMEs and docs name no consuming application." + status: unresolved + verification: judgment +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format; no story is invented, see plan 01): console commands reach the Artisan bar; a user sees what a command did, can ask for more or less of it, and learns from `--help` how to use it. + +This plan's slice (plan 3 of 4, wave 3): the sweep. The tracer takes `serve` all the way: help and examples, the startup summary as a block and as log records, the `http` request log by route pattern, the shutdown report and exit 0. Then the other long-running commands (`queue:work`, `schedule:run`, `summer dev` running `serve`), the application binary's one-shot commands with the Artisan checklist and the D-19 hints, the `summer` tool's commands with the failing framework coverage gate, and the landing site and roadmap deck. A human checkpoint (D-26) stands before anything leaves the machine. After the go-ahead: tag and push the framework with the changelog post, sweep and push the shared plugins one commit each, and bump the applications' pointers in separate commits. + +## Spec-less probe fallback + +Spec-less probe fallback skipped: no requirement IDs (phase_req_ids null) and no SPEC.md; visible choice. `must_haves` cite SC-3..SC-5 and D-04..D-29. + +## Decisions taken in this plan (recorded assumptions and Claude's discretion) + +- Confirmation rule (D-21 with D-28, following Artisan's ConfirmableTrait): migrate, migrate:rollback, queue:clear and deferred:purge ask only when the environment is `production` and the Output is interactive (`bonfire.ConfirmProceed`); they gain `--force` to skip the question. Everywhere else they behave as today. +- `--pretend` (Artisan's name) is added where it can be read-only without touching the command's write path: migrate (lists pending migration IDs per plugin), migrate:rollback (names the migration that would roll back), deferred:purge (counts expired bindings past the cut-off with one read query). The importers already have `--dry-run`; no alias is added (RESEARCH Pitfall 11). Bulk writers whose writes run through a service (paymentgateway's cancel and finish commands, the rewrite-context-types commands, oauthserver:prune, chatvideo:reconcile-sessions, user:backfill-handles) are recorded as declined: a faithful dry run needs a dry mode inside the service, which is command logic and outside this phase (CONTEXT: command logic does not change). +- On a terminal the startup summary is a block on stdout and its log records are tagged with `diary.HideOnTTY(ctx)` so the styled console does not print the same facts twice; daily files and non-terminal consoles keep the records (D-05). Under `-q` the block is not printed. +- The database line prints host, port and database name parsed with pgconn.ParseConfig, never the user, password or full DSN; a DSN that does not parse shows "configured". +- In-flight jobs at stop are reported as "handed back to the queue" after the executor confirms in River's source that StopAndCancel leaves cancelled jobs retryable (RESEARCH A2); if River discards them, the wording becomes "cancelled". +- The framework release version is chosen at the Task 6 checkpoint; the recommendation is the next minor above the latest tag at that time (a new module, new dependencies, new config keys and a new generated main). `docs/setup/upgrading.md` gets a section for that version. +- The user plugin's checkout (`../fonoteka.go/plugins/golem15/user`) is shared with Phase 19.5, which is executing. It is swept last in Task 8, only when 19.5-04-SUMMARY.md exists and the checkout is clean and in sync with origin; otherwise it is deferred: Task 8 files `.planning/todos/pending/user-plugin-console-sweep.md` with a g15office task (subtask of the Phase 19.8 milestone task), the SUMMARY and the verification report mark D-20 as partial and name both, and sm-bm-app, which mounts only the user plugin, is left untouched and listed in the todo (no breaking change either way). +- Plugin descriptions are normalised to imperative sentence case without a trailing period and without planning IDs ("(D-15)", "(D-21)"); names and flags stay. + +## Execution segments (fresh-context resume at the Task 6 checkpoint) + +The phase keeps 4 plans (user-locked), so this plan's size is handled inside the plan: it runs in two executor contexts, split at the release checkpoint that already stops execution. The plugin sweep therefore starts on fresh context instead of last in a degraded one. + +- Segment A: Tasks 1-5 and the Task 6 checkpoint (summercms.go, the landing repository, sm-demo-app; about 190k of the 320k estimate). Before returning the Task 6 checkpoint, the executor writes the `## Handoff` section (below) into the draft `19.8-03-SUMMARY.md` on disk. The draft is not committed here, so it adds nothing to the origin/master..master range shown at the checkpoint; the plan's final SUMMARY commit carries it. +- Segment B: Tasks 7 and 8 (about 130k) in a fresh executor context. Under `/msd-execute-phase` this is the continuation agent spawned after the reply (never a resume of the segment A agent); in inline execution, `/clear` and resume the plan at Task 7. Segment B loads only: the handoff, this plan's Decisions, the shared-plugin rows of the Artisan checklist, Tasks 7 and 8, CONTEXT D-20 to D-28, CLAUDE.md, and RESEARCH "Apps that need pointer bumps" with Pitfalls 15 and 16. It does not reread the plan 01 and 02 SUMMARYs or the framework diffs; the handoff names the API it needs. Its first step checks the handoff against the repositories (Task 7 action). +- Handoff contents (the draft SUMMARY's `## Handoff`): (1) segment A's commits per repository, hash and subject, and the summercms.go origin/master..master list shown at the checkpoint; (2) the framework API the plugin sweep uses, with the names as built (bonfire.Example and its fields, Command.Examples, Command.Hidden, the CheckDocs signature, Reported, WithHint, IsQuiet, the progress and Process helpers, diary.For); (3) the recommended tag and the upgrading section heading; (4) for each plugin repository and application: path, branch, HEAD, uncommitted paths, origin/master..master count; (5) Docker tests run or NOT RUN; (6) whether 19.5-04-SUMMARY.md exists and the user plugin checkout's state; (7) deviations from this plan so far. After the reply the continuation adds the "Task 6 reply" verbatim; Task 7 appends its results before Task 8 starts, so Task 8 can also resume from the handoff alone. + +## Artisan checklist (D-21), per command + +Columns: Force = confirmation with `--force`; Pretend = `--pretend` or an existing dry run; Json = `--json`; Prog = progress bar, process view or table; Result = result line and exit code. "add" means this plan implements it; "–" means it does not fit, with the reason in Notes. Every command also gets Long and Examples (D-17). + +Framework, application binary (Task 1 to Task 3): + +| Command | Force | Pretend | Json | Prog | Result | Notes | +|---|---|---|---|---|---|---| +| serve | – | – | – | startup summary, shutdown report | exit 0 graceful, 1 drain error | long-running (Task 1) | +| queue:work | – | – | – | summary, report | exit 0 graceful, 1 drain error | long-running (Task 2) | +| schedule:run | – | – | – | summary, report | `--once`: "Ran N due entries (M failed)", exit code unchanged from today | long-running (Task 2) | +| migrate | add (production + TTY) | add | – | one line per plugin with applied count | "Applied N migrations" or "Nothing to migrate" | deploys over SSH never prompt | +| migrate:rollback | add (production + TTY) | add | – | – | "Rolled back of " | | +| migrate:status | – read-only | – | add | table kept | – | | +| deferred:purge | add (production + TTY) | add (count only) | – | – | existing purge line | | +| key:generate | – | – | add | – | key on stdout, bare key under -q (D-27) | non-TTY output unchanged | +| queue:clear | add (production + TTY) | – | – | – | "Cleared N jobs from " | description loses its trailing period | +| route:list | – | – | add | table kept | – | | +| admin:create | – | – | – | – | existing success line | prompt rules from plan 02 (piped password kept) | +| admin:reset-password | – | – | – | – | existing | | +| admin:token:create | – | – | add | – | token on stdout (data, kept under -q) | | +| admin:token:list | – | – | add | table kept | – | | +| admin:token:revoke | – single explicit id | – | – | – | existing | | +| admin:token:scopes | – | – | add | table kept | – | | +| websockets:health | – | – | add | – | exit 1 when unhealthy | | +| websockets:generate-vapid-keys | – | – | – | – | Reported replaces the print-then-sentinel error | flags unchanged | +| websockets:test-push | – | – | – | – | Reported | Confirm default stays true under -n | +| greeter:hello (examples/hello) | – | – | – | – | – | Long, Examples | +| blog:publish (docs/examples/blog) | – | – | – | – | existing | Long, Examples | + +Framework, summer tool (Task 2 and Task 4): + +| Command | Force | Pretend | Json | Prog | Result | Notes | +|---|---|---|---|---|---|---| +| build | – | – | – | spinner around go build on a terminal | "Built bin/ in " | | +| make:plugin, make:model, make:migration, make:command, make:job, make:admin-controller | – | – | – | – | "Created " per file | D-19 hints (folded todo, messages only) | +| plugin:add | – | – | – | – | result line | hint when not in an application | +| new | – | – | – | – | existing next steps; writes config/log.yaml (plan 01) | | +| dev | – | – | – | header, rebuild lines, shutdown report | exit 0 on Ctrl+C | runs serve (D-25) | +| parity:proxy, parity:record, parity:replay, parity:broadcasts, parity:upstream | – | – | – | – | existing | Long, Examples; defaults shown by help | +| docs:build, docs:sync | – | `--check` exists | – | – | Reported after the problem list | | +| docs:serve | – | – | – | – | exit 0 on Ctrl+C | | +| 15 delegates (migrate … admin:token:scopes) | – the app binary decides | – | – | – | the app binary's exit code | Examples per delegate row | + +Shared plugins (Tasks 7 and 8; existing flags unchanged): + +| Command | Force | Pretend | Json | Prog | Result | Notes | +|---|---|---|---|---|---|---| +| pg:cancel-created-payments, pg:cancel-sent-payments, pg:cancel-placed-orders, pg:finish-orders | – scheduled state transitions | – declined (service dry mode needed) | – | – | "Cancelled N payments" / "Cancelled N orders" / "Finished N orders" | scheduled daily, run through Call | +| pg:update-currencies | – (`--force` keeps "force update") | – | – | – | "Updated N currencies" | every 10 minutes | +| paymentgateway:import-winter, pgstripe:import-winter, oauthserver:import-winter, sitemap:import-winter, faq:import-winter, menu:import-winter | – (`--force` keeps "replace") | already `--dry-run` | – | – | existing summary lines, final result line | | +| oauthserver:prune | – | – declined | – | – | "Pruned N codes, M tokens, K keys" | scheduled daily | +| oauthserver:rotate-keys | – | – | – | – | "Promoted key " | | +| journal:export-posts, journal:import-posts | – (`--force` keeps "update existing") | already `--dry-run` | – | add progress over files | final counts line | | +| sitemap:import | – | – | – | – | "Imported N URLs" | | +| chat:, chatchannels:, forum:rewrite-context-types | – idempotent | – declined | – | – | "Rewrote N rows" | descriptions lose "(idempotent)." wording into Long | +| chatvideo:reconcile-sessions | – | – declined | – | – | "Ended N sessions" | | +| herald:vapid-generate | – | – | add | – | env lines on stdout (data, kept under -q) | | +| golem:import-settings, feedback:import-settings | – (`--force` keeps its meaning) | – | – | – | result line | | +| user:token:create | – | – | add | – | token on stdout (data) | | +| user:token:list | – | – | add | table kept | – | | +| user:token:revoke, user:require-password-change | – | – | – | – | result line | planning IDs removed from descriptions | +| user:backfill-handles | – | – declined | – | add progress | "Assigned N handles" | | + +## Artifacts this phase produces (this plan) + +- surf: unexported summary and report builders and a request counter in `modules/surf/summary.go`; request log on component `http` with `request_id`; `http.access_log` defaults to on; smoke `TestServeSummarySmoke`, `TestShutdownReportSmoke`; Docker test `TestServeLifecycle`; `route:list --json`. +- lagoon: `MigrationRef{Plugin, ID string}`, `PendingMigrations(gdb *gorm.DB, plugins []party.Plugin) ([]MigrationRef, error)`; flags `migrate --pretend --force`, `migrate:rollback --pretend --force`, `migrate:status --json`, `deferred:purge --pretend --force`, `key:generate --json`; hinted errors for an empty DSN and an unreachable database. +- conga: `WorkerStats{Done, Failed, InFlight int64}`, `(*Worker).Stats() WorkerStats`; `queue:clear --force`; queue:work and schedule:run summary and report. +- cabana: `admin:token:create --json`, `admin:token:list --json`, `admin:token:scopes --json`. lighthouse: `websockets:health --json`. flare: Reported errors. +- diary: `HideOnTTY(ctx context.Context) context.Context`. +- bonfire: wrong-directory hint for application binaries inside RenderError and Fail. +- internal/dev: `Watch(ctx context.Context, appDir string, out bonfire.Output, args []string) error` runs `bin/ serve ` and prints a report; `startBin` passes `serve`. +- cmd/summer: `TestCommandsDocumented` (fails on any CheckDocs problem); Examples for every tool command and delegate. +- Each shared plugin repo: `commands_documented_test.go` with a warn-only `TestCommandsDocumented`; a package-level `diary.For("")` logger; go.mod requires the released framework tag. +- Landing copy (`features.console`, `winter.concepts.commands`) and roadmap deck text in sm-demo-app. + + +Sweep every command to the Artisan bar (help, examples, checklist, result lines, hints), give the long-running commands a startup summary and a shutdown report, move the request log onto the http component, describe it all in the docs, landing site and roadmap deck, then release in D-26 order after a human go-ahead. + +Purpose: SC-3 (coverage gate), SC-4, SC-5 and D-04..D-07, D-11, D-17..D-29. +Output: framework commits per task with READMEs and docs, local landing and deck commits, the recorded checkpoint reply, the tag and changelog post, one commit per shared plugin, pointer-bump and regenerate commits per application. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/19.8-console-commands-overhaul/19.8-CONTEXT.md +@.planning/phases/19.8-console-commands-overhaul/19.8-RESEARCH.md +@.planning/phases/19.8-console-commands-overhaul/19.8-PATTERNS.md +@.planning/phases/19.8-console-commands-overhaul/19.8-01-SUMMARY.md +@.planning/phases/19.8-console-commands-overhaul/19.8-02-SUMMARY.md + +Repos and concurrency: Tasks 1-4 write in summercms.go only; Task 5 in ../sm-summercmsio-app (and its vue-summercmsio-app submodule) and ../sm-demo-app; Tasks 7-8 in the shared plugin repos and the applications. Before each task run `git status --porcelain` in every repository the task touches and `git log --oneline origin/master..master` (after `git fetch origin`) in each plugin and application; stop and report when a path this plan edits is modified by someone else or a repository holds commits this plan did not make (except summercms.go master, whose unpushed commits are listed at the checkpoint). Superprojects (sm-demo-app, sm-bm-app, sm-grzybyfunkcjonalne-app, sm-summercmsio-app, fonoteka.go) often carry other sessions' pointer changes: stage only the submodule paths and files this plan names. Stage by explicit path; no co-author tags; planning docs and code in separate commits; READMEs and docs in the same commit as the change they describe; `go vet ./...`, `go test -short ./...` and `go -C examples/hello test ./...` green at every framework commit. Docker-backed tests skip under -short; on a Docker host run them, through `sudo -u golem` if the socket is denied (CLAUDE.md); a Docker test that could not run is NOT RUN in the SUMMARY, never passed. + + +- plan 02 API: `bonfire.Example`, `Command.Examples`, `Command.Hidden`, `WithHint`, `Reported`, `Usage`, `ConfirmProceed`, `Process`, `Current`, `GlobalsOf`, `IsQuiet`, `Interactive`, `Globals.Args`, `CheckDocs`, `RenderError`, `Fail`; plan 01 API: `diary.For`, `diary.Bootstrap`, `diary.Verbosity`, `diary.NewConsoleHandler`. +- modules/surf/serve.go:28-122 ServeCommand (NotifyContext, OpenFromApp, Publish, publishUploads, installCooler, BuildRouter, compile, net.Listen, StartServeWorker, banner, http.Server, 10 s shutdown, stopWorker), serveURL :178, countAdmins :212, serveBanner :243 (`"listening on " + addr`), printBanner :273; modules/surf/serve_test.go TestServeBanner. +- modules/surf/accesslog.go: accessLogKey "http.access_log", accessLogMessage "http request", accessEntry{pattern, panicked}, accessLogEnabled :38, the doc comment :42-60 (why paths are never logged), attrs :74-79, accessLogger :101-108; modules/surf/errorpage.go errorPageMaxRequestID = 128 and its request-id trimming. +- modules/lagoon/migrations.go: Migrate :64, RollbackLast :121, lastMigrationPlugin :156, StatusRow :169, Status :176; modules/lagoon/commands.go migrate :26, rollback :39, status :60, deferred:purge :82 (PurgeDeferred, PurgeResult{Bindings, Files, Children, Skipped}); modules/lagoon/keygen.go :24-37; modules/lagoon/connection.go:22-24 empty-DSN error, :30-32 ping error. +- modules/conga/worker.go: Worker.Queues :44, StartWorker :63, StartServeWorker :163, Stop :172 (StopAndCancel after the deadline), Manager.runAttempt :243; modules/conga/commands.go queue:work :31, schedule:run :45, queue:clear :64, work :84, runScheduler :100. +- modules/cabana/token_commands.go:21-60 and :117 (`out.Println(plain)` for the token); modules/cabana/commands.go:21-45; modules/lighthouse/centrifugo/commands.go:31; modules/flare/commands.go:20-21 (errGenerateFailed, errTestPushFailed), :50, :212. +- internal/dev/watch.go: Options{Build, Start, Debounce} :21, Watch :29, rebuild output via fmt.Fprintf :79-88, startBin :201 (`exec.Command(binPath)`); internal/dev/watch_test.go fakes Start. +- cmd/summer/main.go: toolCommands :28, make* :65-211, resolveMakeArgs :220, printArtifact :245, devCommand :255, addPluginCommand :269, newCommand :296; cmd/summer/runtime.go delegateCommands table :26-44 and delegateCommand :55; cmd/summer/docs.go docs:build/sync/serve and reportDocsProblems :168 (`errors.New(cmd + " failed")`); cmd/summer/parity.go flags; internal/build/manifest.go:215 (`build: no summer.yaml found from`). +- examples/hello/plugins/greeter/plugin.go:80 greeter:hello; docs/examples/blog/console/publish.go:18 blog:publish. +- Sibling repos: plugin constructors in each ../sm-*-plugin/console/*.go (analog ../sm-sitemap-plugin/console/import.go:27-45); the user, golem and feedback plugins under ../fonoteka.go/plugins/golem15/; application submodules per RESEARCH "Apps that need pointer bumps"; ../sm-summercmsio-app/scripts/changelog-post.py and DEPLOY.md ("Posting to the changelog"); ../sm-demo-app/scripts/sync-roadmap.sh and README "Roadmap deck"; ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json (`features.console`, `winter.concepts.commands`). + + + + + + + Task 1: Tracer: serve shows what it booted, logs each request by route pattern under the http component, reports what it did on Ctrl+C and exits 0, with help, examples and docs + D-23 flips the request log to on by default and its record shape (component http, request_id) becomes what operators grep and ship to log stores. + modules/surf/serve.go, modules/surf/summary.go, modules/surf/accesslog.go, modules/surf/serve_summary_smoke_test.go, modules/surf/serve_lifecycle_test.go, modules/surf/README.md, modules/lagoon/migrations.go, modules/lagoon/README.md, modules/conga/worker.go, modules/conga/README.md, modules/diary/diary.go, modules/diary/console.go, modules/diary/README.md, docs/console/setup-and-maintenance.md, docs/services/logging.md + modules/surf/serve.go, modules/surf/accesslog.go, modules/surf/accesslog_test.go (TestAccessLogSmoke), modules/surf/serve_test.go, modules/surf/errorpage.go (request id cap), modules/lagoon/migrations.go, modules/conga/worker.go, modules/conga/postgres_test.go (testcontainers Postgres setup to copy), modules/cooler/cooler.go (CacheDriverKey), modules/postcard/mailer.go (mail.driver default), modules/beachcomber/beachcomber.go (search.driver), modules/lighthouse/lighthouse.go (realtime.driver), modules/conga/conga.go (queue.work_in_serve, queue.queues), modules/diary/console.go, docs/console/setup-and-maintenance.md ("The HTTP server"), docs/services/logging.md ("Request log"), ../sm-grzybyfunkcjonalne-app/scripts/smoke.sh (:292-306, 2>&1 and the "listening on" grep), 19.8-RESEARCH.md (Pattern 6, Pitfall 9), 19.8-CONTEXT.md (D-04..D-07, D-23, D-29) + Per D-04..D-07, D-11, D-23 and D-29. + +lagoon/migrations.go: `type MigrationRef struct { Plugin, ID string }` and `PendingMigrations(gdb, plugins)` returning each plugin's declared migration IDs that its history table does not hold, in plugin and declaration order (reuse Status's table reading; a missing history table means all pending). README API row. + +conga/worker.go: `type WorkerStats struct { Done, Failed, InFlight int64 }`; atomic counters updated in Manager.runAttempt (InFlight up on entry and down on exit; Done on a nil error, Failed otherwise); `(*Worker).Stats()` returns them. Confirm in River's source how StopAndCancel leaves cancelled jobs (RESEARCH A2) and word the report accordingly. README API row. + +diary: `HideOnTTY(ctx)` returns a context carrying a marker; a console channel that renders styled lines on a terminal skips records whose context carries it; every other channel and a non-terminal console write them. README row. + +surf/summary.go (new, unexported): a request counter wrapping the compiled handler (atomic in-flight and served counts, independent of the access-log setting); `summaryFacts` collected once after binding: app name (app.name) and `bonfire.BuildVersion()`, environment (`app.Config.Environment()`), URL (serveURL) and admin URL with admin presence (existing countAdmins), database host, port and name from pgconn.ParseConfig of database.dsn (never user or password; "configured" when it does not parse), pending migrations from lagoon.PendingMigrations ("up to date" or "N pending — run bin/ migrate"), plugins (count and IDs), queue (worker queues or "off"), scheduler (in serve or off, as conga runs it), mail (mail.driver), cache (cooler.CacheDriverKey), realtime (realtime.driver), search (search.driver), each defaulting the way its module does; a block renderer (8 to 12 aligned label/value lines in the bonfire palette; with verbosity 1 or more one extra line per plugin with its ID, required plugins and pending migration count) and a records emitter (one INFO record "startup summary" from `diary.For("serve")` with one attribute per fact, and one INFO record whose message is exactly `listening on `); a report builder (draining, in-flight requests drained or aborted at the deadline, served total, worker Done/Failed/InFlight, uptime, drain errors) rendered as lines and as one INFO record "shutdown report". + +surf/serve.go: wrap the handler with the counter; after the listener and worker start, on a non-quiet terminal (`!bonfire.IsQuiet(out)` and the console is a terminal) print the block and emit the records with `diary.HideOnTTY(ctx)`; otherwise emit the records only (the old ℹ banner lines and the separate "job worker started" Info line are folded into the summary). On ctx.Done (signal): print "Shutting down: draining N requests" (decoration, suppressed under -q), Shutdown with the 10 s deadline, stop the worker, print and log the report, and return nil when there were no drain errors (D-29), else the joined drain errors (exit 1). A server error unrelated to a signal keeps today's behaviour. Add Long (what serve boots, how it shuts down, exit codes) and Examples (`serve`, `serve --addr 127.0.0.1:8096`, `serve -v` for per-plugin details, `serve -q` for warnings and errors only). + +surf/accesslog.go (D-23): logger is the app-published `*slog.Logger` when present, else `diary.For("http")`; attributes method, pattern, status, duration_ms, request_id (X-Request-Id trimmed and capped at 128 bytes, omitted when empty), client_gone as today; the URL path, query, headers and remote address stay out (keep the doc comment's reasoning and add D-23); accessLogEnabled returns true unless `http.access_log` is set to false. Update TestAccessLogSmoke expectations for request_id and the default. + +Docs and READMEs in the same commit: docs/console/setup-and-maintenance.md "The HTTP server" (the summary block, what -v and -q change, the shutdown report, exit codes, the records off a terminal); docs/services/logging.md "Request log" (on by default at INFO under component http, fields, why the route pattern and never the path, silencing with `components: {http: warn}` or `http.access_log: false`); surf README (Features, Configuration default for http.access_log). Commit: `feat(surf): serve startup summary, shutdown report and http request log`. + +Tests. serve_summary_smoke_test.go (-short): `TestServeSummarySmoke` builds the facts from a compass config with a DSN containing a password and checks the block lines (environment, URL, database host and name, "3 pending", plugin count, drivers) and the record attributes, that the password and user never appear, and that one record's message equals `listening on 127.0.0.1:8096`; `TestShutdownReportSmoke` renders the report for fixed counters and a drain error and checks each line. serve_lifecycle_test.go (Docker, skips under -short through a TestMain that calls flag.Parse and testing.Short, or the package's existing TestMain): `TestServeLifecycle` starts Postgres with testcontainers, runs ServeCommand's Run with `diary.Bootstrap` on a buffer, a file:// uploads bucket in t.TempDir and an addr on 127.0.0.1:0-free port, waits for the listener, sends one request with X-Request-Id, cancels the context, and asserts Run returned nil, the buffer holds `listening on 127.0.0.1:`, "startup summary", one "http request" record with component http, pattern and request_id and without the request path's literal value, and "shutdown report" with served 1. + + go vet ./... && go test -count=1 -short ./modules/surf/... ./modules/lagoon/... ./modules/conga/... ./modules/diary/... && go test -count=1 -short -run '^(TestServeSummarySmoke|TestShutdownReportSmoke|TestAccessLogSmoke|TestServeBanner)$' -v ./modules/surf 2>&1 | grep -c -- '^--- PASS: Test' | grep -qE '^[34]$' && go -C examples/hello test -count=1 ./... && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check + Non-zero exit, or fewer than 3 top-level "--- PASS: Test" lines (the summary, report or access-log smoke failed or did not run; TestServeBanner may be removed if the banner builder is folded into the summary), or hello or the docs checker fail. + go test -count=1 -run '^TestServeLifecycle$' -v ./modules/surf 2>&1 | grep -q -- '^--- PASS: TestServeLifecycle' + No "--- PASS: TestServeLifecycle" line: the end-to-end serve run failed, or it printed "--- SKIP" because Docker was unreachable (rerun through sudo -u golem; otherwise record NOT RUN in the SUMMARY). + + + - `grep -c 'diary.For("http")' modules/surf/accesslog.go` prints 1 and `grep -c '"request_id"' modules/surf/accesslog.go` prints at least 1. + - `grep -cE 'r\.URL\.Path|req\.URL\.Path|RequestURI' modules/surf/accesslog.go` prints 0. + - `grep -c 'func PendingMigrations(' modules/lagoon/migrations.go` prints 1 and `grep -c 'func (w \*Worker) Stats() WorkerStats' modules/conga/worker.go` prints 1. + - `grep -c '"listening on "' modules/surf/summary.go` prints at least 1. + - `grep -c 'Examples:' modules/surf/serve.go` prints at least 1 and `grep -c 'Long:' modules/surf/serve.go` prints at least 1. + - `grep -c 'components' docs/services/logging.md` prints at least 1 and `grep -c 'request_id' docs/services/logging.md` prints at least 1. + + An operator starting serve sees what booted and whether migrations are pending, sees each request by route, and on Ctrl+C gets a short report and exit code 0. + + + + Task 2: queue:work and schedule:run report what booted and what they did; summer dev runs bin/<app> serve with the global flags and reports on Ctrl+C; docs:serve stops cleanly + modules/conga/commands.go, modules/conga/README.md, internal/dev/watch.go, internal/dev/watch_test.go, cmd/summer/main.go, cmd/summer/docs.go, docs/console/setup-and-maintenance.md, docs/console/scaffolding.md, docs/console/utilities.md + modules/conga/commands.go, modules/conga/worker.go (Stats from Task 1), modules/conga/scheduler.go, modules/surf/summary.go (Task 1 block and report helpers to mirror), internal/dev/watch.go, internal/dev/watch_test.go, cmd/summer/main.go (devCommand :255), cmd/summer/docs.go (docs:serve), docs/console/setup-and-maintenance.md ("Queues and the scheduler"), docs/console/scaffolding.md ("Building and watching"), docs/console/utilities.md, 19.8-RESEARCH.md (Pitfalls 13 and 14) + Per D-07, D-22, D-25 and D-29. + +conga/commands.go queue:work: after the worker starts, a summary (environment, queues with their concurrency, database host, scheduler included or not) as a block on a non-quiet terminal and as an INFO record "startup summary" from `diary.For("queue")` (HideOnTTY when the block was printed); on signal, the report from `Worker.Stats` (done, failed, handed back), uptime and stop errors as lines and an INFO record "shutdown report"; return nil on a clean stop (D-29). schedule:run: summary (entry count, the next due entry); report (entries run, failures, uptime); `--once` ends with "Ran N due entries (M failed)" and keeps today's error and exit behaviour. Long and Examples for queue:work (`queue:work`, `queue:work --queue mail --queue default`), schedule:run (`schedule:run`, `schedule:run --once` for cron) and queue:clear is left to Task 3. + +internal/dev/watch.go (D-25): `Watch(ctx, appDir, out bonfire.Output, args []string) error`; startBin runs `exec.Command(binPath, append([]string{"serve"}, args...)...)` and Options.Start gains the args; the header ("Watching ; running bin/ serve"), rebuild lines ("Rebuilt in ", "Build failed: " through out.Error, "Restarted bin/") go through Output instead of raw Fprintf; on ctx.Done stop the child with SIGTERM (it prints its own report), then print the dev report (rebuilds, failed builds, restarts, uptime) and return nil. cmd/summer/main.go devCommand: wrap ctx with signal.NotifyContext(os.Interrupt, syscall.SIGTERM) and pass `bonfire.GlobalsOf(out).Args()`; Long (what is watched, that it runs serve, flags forwarded) and Examples (`dev`, `dev -v`). watch_test.go: the fake Start asserts it receives "serve" followed by the forwarded args, and a cancelled context returns nil. + +cmd/summer/docs.go docs:serve: Ctrl+C returns nil; Long and Examples. + +Docs in the same commit: setup-and-maintenance.md "Queues and the scheduler" (summary and report, exit codes); scaffolding.md "Building and watching" (dev runs serve, forwards -v/-q, reports on Ctrl+C); utilities.md docs:serve line; conga README. Commit: `feat(conga,dev): worker and scheduler reports, summer dev runs serve`. + + go vet ./... && go test -count=1 -short ./modules/conga/... ./internal/dev/... ./cmd/summer/... && go test -count=1 -short -v ./internal/dev 2>&1 | grep -q -- '^--- PASS' && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check + Non-zero exit (a conga, dev or summer test is red, for example the fake Start did not receive "serve"), no "--- PASS" line from internal/dev (no test ran), or the docs checker rejects the new pages. + + + - `grep -c 'append(\[\]string{"serve"}, args...)' internal/dev/watch.go` prints 1. + - `grep -cE 'fmt\.Fprintf\(out' internal/dev/watch.go` prints 0. + - `grep -c 'GlobalsOf(out).Args()' cmd/summer/main.go` prints 1. + - `grep -c 'shutdown report' modules/conga/commands.go` prints at least 1. + - `grep -c 'serve' docs/console/scaffolding.md` prints at least 1. + + Every long-running command tells the operator what it started and what it did, summer dev actually serves the application, and Ctrl+C ends each of them with exit code 0. + + + + Task 3: Application-binary commands meet their checklist rows (help, examples, --force, --pretend, --json, result lines) and known failures end in hints, including the wrong-directory hint + modules/lagoon/commands.go, modules/lagoon/keygen.go, modules/lagoon/connection.go, modules/lagoon/README.md, modules/conga/commands.go, modules/surf/routelist_command.go, modules/surf/serve.go, modules/surf/README.md, modules/cabana/commands.go, modules/cabana/token_commands.go, modules/cabana/README.md, modules/lighthouse/centrifugo/commands.go, modules/lighthouse/README.md, modules/flare/commands.go, modules/flare/README.md, modules/bonfire/errors.go, modules/bonfire/README.md, examples/hello/plugins/greeter/plugin.go, examples/hello/hello_test.go, docs/examples/blog/console/publish.go, cmd/summer/commands_documented_test.go, docs/console/setup-and-maintenance.md, docs/console/writing-commands.md + modules/lagoon/commands.go, modules/lagoon/keygen.go, modules/lagoon/connection.go, modules/conga/commands.go (queue:clear :64), modules/surf/routelist_command.go, modules/surf/serve.go (publishUploads error, app.key use), modules/cabana/commands.go, modules/cabana/token_commands.go, modules/cabana/commands_test.go (:241-262), modules/lighthouse/centrifugo/commands.go, modules/flare/commands.go, modules/bonfire/errors.go, cmd/summer/docs.go (docsCommands constructors to reuse in the test), examples/hello/plugins/greeter/plugin.go, docs/examples/blog/console/publish.go, docs/console/setup-and-maintenance.md, this plan's checklist table (application binary rows), 19.8-RESEARCH.md (Command Inventory, Code Examples "Error with hint", Pitfalls 10, 11 and 13) + Implement the application-binary rows of this plan's checklist exactly; every command gets Long and Examples; names and existing flags stay. + +lagoon: migrate gains `--pretend` (print each pending MigrationRef as ": " and "N migrations would run", apply nothing) and `--force` (Bare, "Skip the production confirmation"); before applying, when the environment is production and ConfirmProceed asks and gets no, return a Reported error "Migration cancelled."; result "Applied N migrations" (N from PendingMigrations before Migrate) or "Nothing to migrate". migrate:rollback: `--pretend` names the last applied migration of the plugin (from Status) and `--force` with the same confirmation rule; result "Rolled back of ". migrate:status `--json` prints the rows as a JSON array of {plugin, id, applied_at}. deferred:purge `--pretend` reports the number of bindings older than the cut-off (one COUNT query with the same filter, nothing deleted) and `--force` with the confirmation rule. key:generate `--json` prints {"key": ...}; under -q (bonfire.IsQuiet) the key prints through Println; otherwise unchanged. connection.go: the empty-DSN error becomes WithHint(err, "The application reads database.dsn from config/database.yaml or SUMMER_DATABASE__DSN.", "cp .env.example .env") and the ping failure WithHint(err, "PostgreSQL is not reachable at the configured host.", "docker compose up -d --wait"). Missing uploads bucket and empty app.key errors in serve get hints naming config/storage.yaml and `{bin} key:generate`. + +conga queue:clear: description "Clear pending jobs from a queue" (no trailing period), `--force` with the confirmation rule, result "Cleared N jobs from ". surf route:list `--json` (array of {method, pattern, name, middleware} as the table shows). cabana: admin:token:create `--json` ({id, name, token, scopes, expires_at}; the plain token still prints through Println otherwise), admin:token:list `--json`, admin:token:scopes `--json`. lighthouse websockets:health `--json` ({healthy, ...} without the API key) and exit 1 when unhealthy (as today). flare: replace the print-then-sentinel errors (errGenerateFailed, errTestPushFailed) with `bonfire.Reported(...)` so the renderer adds no second message. + +bonfire/errors.go (D-19, message only): for application binaries (root name other than summer), when a command fails or Fail is called, and the working directory has no `config` directory, and a parent directory holds `summer.yaml`, add a hint "This command runs from the application root." with Try `cd ; bin/ `; summer is unaffected (its commands walk up already). Database detection from the wrong directory is not changed. + +Examples: greeter:hello (examples/hello) and blog:publish (docs/examples/blog) get Long and Examples; examples/hello/hello_test.go gains `TestGreeterDocumented` asserting bonfire.CheckDocs returns nothing for the greeter plugin's commands. + +cmd/summer/commands_documented_test.go (D-17): `TestCommandsDocumented` builds the application command set the way docsCommands does (lagoon, key:generate, conga, surf serve and route:list, cabana, centrifugo, flare) plus blog.Publish, runs bonfire.CheckDocs and fails listing every problem. (Task 4 adds the tool commands.) + +Docs and READMEs in the same commit(s): docs/console/setup-and-maintenance.md (new flags with examples, the production confirmation rule and --force, -q keeps keys and tokens), writing-commands.md (the wrong-directory hint), each touched module README's CLI commands section. Commits by module are fine (`feat(lagoon): checklist flags and hints for migration commands`, `feat(cabana,surf,conga): --json and result lines`, `feat(bonfire): wrong-directory hint`), each green. + + go vet ./... && go test -count=1 -short ./... && go test -count=1 -short -run '^TestCommandsDocumented$' -v ./cmd/summer 2>&1 | grep -q -- '^--- PASS: TestCommandsDocumented' && go -C examples/hello test -count=1 -run '^TestGreeterDocumented$' -v . 2>&1 | grep -q -- '^--- PASS: TestGreeterDocumented' && go -C examples/hello test -count=1 ./... && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check + Non-zero exit; no "--- PASS: TestCommandsDocumented" (an application command lacks a description, long help, examples or a flag description, or the test did not run); no "--- PASS: TestGreeterDocumented"; or the WR-13 piped-password test, hello or the docs checker fail. + + + - `grep -c '"pretend"' modules/lagoon/commands.go` prints at least 3 and `grep -c '"force"' modules/lagoon/commands.go` prints at least 3. + - `grep -c 'ConfirmProceed' modules/lagoon/commands.go` prints at least 1 and `grep -c 'ConfirmProceed' modules/conga/commands.go` prints at least 1. + - `grep -c '"json"' modules/cabana/token_commands.go` prints at least 3 and `grep -c '"json"' modules/surf/routelist_command.go` prints 1. + - `grep -cE 'errGenerateFailed|errTestPushFailed' modules/flare/commands.go` prints 0 and `grep -c 'bonfire.Reported(' modules/flare/commands.go` prints at least 1. + - `grep -c 'WithHint(' modules/lagoon/connection.go` prints 2. + - `grep -c 'Clear all queued jobs, by deleting all pending jobs\.' modules/conga/commands.go` prints 0. + - `grep -c 'bonfire.CheckDocs(' cmd/summer/commands_documented_test.go` prints at least 1. + + Every application-binary command explains itself with examples, scripts get JSON where they read output, destructive commands ask only where a human is watching in production, and the common failures say exactly what to run next. + + + + Task 4: summer tool commands meet their checklist rows and pass the failing coverage gate; make:* errors carry hints; the upgrading notes and console docs are complete + cmd/summer/main.go, cmd/summer/runtime.go, cmd/summer/parity.go, cmd/summer/docs.go, cmd/summer/commands_documented_test.go, internal/build/manifest.go, internal/build/artifact.go, internal/build/scaffold.go, docs/console/introduction.md, docs/console/scaffolding.md, docs/console/utilities.md, docs/console/writing-commands.md, docs/setup/upgrading.md, modules/bonfire/README.md, modules/diary/README.md + cmd/summer/main.go, cmd/summer/runtime.go, cmd/summer/parity.go, cmd/summer/docs.go, cmd/summer/main_test.go, cmd/summer/runtime_test.go, internal/build/manifest.go (:205-220), internal/build/artifact.go, internal/build/scaffold.go, .planning/todos/pending/scaffold-generated-header-and-command-deps.md, docs/console/introduction.md ("Commands that summer delegates"), docs/console/scaffolding.md, docs/console/utilities.md, docs/setup/upgrading.md ("Upgrading to 0.2" structure), this plan's checklist table (summer tool rows) + Implement the summer tool rows of this plan's checklist; names and flags stay. + +build: on a live console wrap the go build in out.Spinner; end with "Built bin/ in ". make:plugin, make:model, make:migration, make:command, make:job, make:admin-controller: one "Created " line per written file (printArtifact), Long (what is generated, where, how the registry picks it up, and for make:command the current limitation that the generated function takes no arguments, from the folded todo), Examples (`make:model acme.blog Post`, `make:command acme.blog publish` and so on). Hints (folded todo, partial, messages only; the scaffolder is unchanged): internal/build/manifest.go's "no summer.yaml found from " becomes WithHint(err, "summer make: commands run inside an application (a directory with summer.yaml).", "cd ; summer "); invalid plugin IDs and identifiers, an artifact that already exists and a plugin that is not in summer.yaml each get an explanation and a Try with a valid form. plugin:add and new: result lines and Examples. parity:* commands: Long (what each does in the parity workflow) and Examples; flag descriptions keep their "(true/false)" wording. docs:build and docs:sync: reportDocsProblems returns `bonfire.Reported(...)` after printing the problem list; Long and Examples. Delegates (runtime.go): the table gains an examples column, each delegate gets two Examples rendered with summer (for example `migrate`, `migrate --pretend`; `admin:token:create jane --scope journal.posts --json`), and Long adds that global flags pass through to the application binary. + +cmd/summer/commands_documented_test.go: extend `TestCommandsDocumented` to `toolCommands()` as well; it fails on any problem. + +docs/setup/upgrading.md: a section "Upgrading to 0.4" (Task 7 renames it if the user picks another version) with Boot refusals and Behaviour changes in the existing structure: run `summer build` to regenerate main.go; daily log files under storage/logs (directory must be writable, or set log.default to console); GORM lines move to component db and record-not-found is no longer logged; the request log is on by default (route pattern, silence with components.http: warn); duplicate command names and reserved global flag names stop the binary at boot; usage errors exit 2; -q keeps data output; summer dev runs serve; destructive commands confirm in production on a terminal (--force skips). docs/console/introduction.md "Commands that summer delegates" (examples and global flags), scaffolding.md (make:* results and hints), utilities.md (parity and docs examples), writing-commands.md (a complete example command with Long and Examples). Commit: `feat(summer): help, examples and hints for every tool command`. + + go vet ./... && go test -count=1 -short ./cmd/summer/... ./internal/build/... && go test -count=1 -short -run '^TestCommandsDocumented$' -v ./cmd/summer 2>&1 | grep -q -- '^--- PASS: TestCommandsDocumented' && go test -count=1 -short ./... && go -C examples/hello test -count=1 ./... && go test -count=1 -run '^TestDocsTree$' ./cmd/summer && go run ./cmd/summer docs:build --check + Non-zero exit, or no "--- PASS: TestCommandsDocumented" (a tool or delegate command lacks a description, long help or examples), or the build, summer, hello or docs checks fail. + + + - `grep -c 'toolCommands()' cmd/summer/commands_documented_test.go` prints at least 1. + - `grep -c 'WithHint(' internal/build/manifest.go` prints at least 1. + - `grep -c 'bonfire.Reported(' cmd/summer/docs.go` prints at least 1 and `grep -c 'errors.New(cmd + " failed")' cmd/summer/docs.go` prints 0. + - `grep -c '^## Upgrading to 0.4' docs/setup/upgrading.md` prints 1 and the section matches each of `storage/logs`, `summer build`, `components`, `exit` (checked with grep -c on the file, each at least 1). + - `go run ./cmd/summer list --format=json | python3 -c 'import json,sys; d=json.load(sys.stdin); bad=[c["name"] for c in d["commands"] if not c["examples"] and c["name"] not in ("help","list")]; sys.exit(1 if bad else 0)'` exits 0. + + summer's own commands, including every delegate, explain themselves with examples, make:* failures say where to run them, and the upgrading page lists every behaviour change of this phase. + + + + Task 5: The summercms.io landing copy and the roadmap deck in sm-demo-app describe the new console (local commits, nothing pushed or deployed) + ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json, ../sm-summercmsio-app/vue-summercmsio-app, ../sm-demo-app/plugins/demo/roadmap/curated.go, ../sm-demo-app/plugins/demo/content/ROADMAP.md + ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json (features.console :117-120, winter.concepts.commands :139), ../sm-summercmsio-app/vue-summercmsio-app/app/components/FeaturesSection.vue, ../sm-summercmsio-app/vue-summercmsio-app/app/data/terminal.json, ../sm-summercmsio-app/terminal_check_test.go, ../sm-summercmsio-app/CLAUDE.md, ../sm-demo-app/README.md ("Roadmap deck"), ../sm-demo-app/plugins/demo/roadmap/curated.go (console feature lines near :104 and :145), ../sm-demo-app/plugins/demo/roadmap/roadmap_test.go, ../sm-demo-app/scripts/sync-roadmap.sh + Per SC-5. Landing (vue-summercmsio-app): `features.console.body` names the new console in one sentence of the same length class (Artisan-style list and help with examples, -v and -q on every command, daily log files); `winter.concepts.commands` keeps "Artisan commands" on the Winter side and shows "bonfire.Command + summer list" (or similar short text) on the Summer side. terminal.json stays unchanged (its commands still work). Run `npm --prefix ../sm-summercmsio-app/vue-summercmsio-app run test`. Commit in vue-summercmsio-app (`feat: describe the Artisan-style console`), then in sm-summercmsio-app stage only the `vue-summercmsio-app` submodule path and commit the pointer (`chore: bump vue-summercmsio-app for console copy`). + +Roadmap deck (sm-demo-app): in plugins/demo/roadmap/curated.go update the console feature text (EN and PL) to name the Artisan-style list and help, global flags, the diary logging with daily files and the startup summary; refresh the snapshot with `scripts/sync-roadmap.sh` (from framework master as of now) and run `go -C ../sm-demo-app test ./plugins/demo/roadmap/...` (TestRoadmapNoClientNames, TestRoadmapPolishTitles must pass). Commit in sm-demo-app staging only those two paths (`feat(demo): roadmap deck describes the new console`). Nothing is pushed or deployed here; Task 7 or 8 pushes under the Task 6 reply. + + npm --prefix ../sm-summercmsio-app/vue-summercmsio-app run test && go -C ../sm-summercmsio-app test -count=1 -run '^(TestLoadTerminal|TestTerminalScript|TestTerminalEnv)$' . && go -C ../sm-demo-app test -count=1 ./plugins/demo/roadmap/... && ../sm-demo-app/scripts/sync-roadmap.sh --check + Non-zero exit: a landing node test fails, the terminal data no longer loads, a roadmap test fails (for example a client name slipped into the deck text), or sync-roadmap --check reports the snapshot stale. + + + - `grep -c 'Scaffolding, scheduler, workers and your own commands.' ../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json` prints 0. + - `L=$(git -C ../sm-summercmsio-app/vue-summercmsio-app log --oneline -1 -- i18n/locales/en.json) && test -n "$L"` exits 0 and `S=$(git -C ../sm-summercmsio-app status --porcelain -- vue-summercmsio-app) && test -z "$S"` exits 0 (pointer committed). + - `S=$(git -C ../sm-demo-app status --porcelain -- plugins/demo/roadmap plugins/demo/content/ROADMAP.md) && test -z "$S"` exits 0. + - `B=$(git -C ../sm-summercmsio-app/vue-summercmsio-app branch -r --contains HEAD) && test -z "$B"` exits 0 (not pushed yet). + + The landing copy and the roadmap deck describe the new console, committed locally and ready to publish with the release. + + + + Task 6: Go-ahead for the release in D-26 order: framework tag, push and changelog post, then the shared plugins' commits and pushes, then the applications' pointer bumps + 19.8-CONTEXT.md (D-20, D-24, D-26), this plan's "Execution segments" section (handoff contents), this plan's SUMMARY draft (Tasks 1-5), CLAUDE.md (Commits and concurrency, the changelog rule), ../sm-summercmsio-app/DEPLOY.md ("Posting to the changelog"), .planning/phases/19.5-social-login-in-sm-user-plugin/ (whether 19.5-04-SUMMARY.md exists) + + - Before the checkpoint is returned, the draft SUMMARY holds the handoff (items 1-7 of "Execution segments"): `grep -c '^## Handoff' .planning/phases/19.8-console-commands-overhaul/19.8-03-SUMMARY.md` prints 1. + - The SUMMARY records the reply verbatim under a "Task 6 reply" heading, and the chosen tag when the reply is a release. + - Before the reply, `T=$(git tag --points-at HEAD) && test -z "$T"` exits 0 in summercms.go and no plugin repository has a commit from this plan (`git -C ../sm-journal-plugin log --oneline origin/master..master` lists none of this phase's subjects). + + May the phase publish now, and with which framework tag? + Tasks 1-5 are committed locally and green (the SUMMARY draft lists the commits, the Docker results and anything NOT RUN). The shared plugins cannot be swept until a framework tag exists, because each plugin's single commit (D-20) requires the tag that carries bonfire.Example and diary. Facts to weigh: summercms.go master is ahead of origin/master by this phase's commits plus other sessions' unpushed commits (at planning time 20, mostly Phase 19.5); pushing master publishes all of them, and the tag needs a same-day changelog post (scripts/changelog-post.py in ../sm-summercmsio-app; the token file must exist). The latest tag at planning time was v0.3.1; a new module, new dependencies, new config keys and a new generated main suggest the next minor (v0.4.0), unless another phase tagged in between. After the tag: 15 plugin repositories get one commit each and are pushed (sm-user-plugin only if Phase 19.5 has finished with it; otherwise it is deferred to a tracked todo and g15office task and D-20 is reported partial); then sm-demo-app, sm-bm-app, sm-grzybyfunkcjonalne-app, sm-summercmsio-app and fonoteka.go get pointer-bump commits and a regenerated main.go; the landing and deck commits from Task 5 go out with their repositories. Nothing is deployed in any option. This checkpoint ends segment A: the handoff is written to the SUMMARY draft before it returns, and after the reply Tasks 7 and 8 run in a fresh executor context from that handoff (see Execution segments). + + + + + + Reply "release vX.Y.Z", "release-keep-apps-local vX.Y.Z" or "hold". A release reply also authorises pushing each plugin repository after its single commit and, for "release", pushing each application, landing and deck repository whose unpushed commits are this phase's; nothing is ever force-pushed or deployed. The continuation runs Tasks 7 and 8 in a fresh context from the SUMMARY draft's handoff. + + + + Task 7: Under the Task 6 reply, tag and push the framework with the changelog post, then sweep and push paymentgateway, pgstripe, oauthserver, journal, sitemap, faq and menu, one commit each pinned to the tag + 19.8-03-SUMMARY.md (draft) holds the `## Handoff` section from segment A and records the Task 6 reply "release vX.Y.Z" or "release-keep-apps-local vX.Y.Z"; on "hold" this task records "release held", notes that the local Task 5 deck and landing commits describe an unreleased console, and does nothing else. + docs/setup/upgrading.md, ../sm-paymentgateway-plugin/console/pg_cancel_created_payments.go, ../sm-paymentgateway-plugin/console/pg_cancel_sent_payments.go, ../sm-paymentgateway-plugin/console/pg_cancel_placed_orders.go, ../sm-paymentgateway-plugin/console/pg_finish_orders.go, ../sm-paymentgateway-plugin/console/pg_update_currencies.go, ../sm-paymentgateway-plugin/console/import_winter.go, ../sm-paymentgateway-plugin/commands_documented_test.go, ../sm-paymentgateway-plugin/README.md, ../sm-paymentgateway-plugin/go.mod, ../sm-paymentgateway-plugin/go.sum, ../sm-pgstripe-plugin/console/import_winter.go, ../sm-pgstripe-plugin/commands_documented_test.go, ../sm-pgstripe-plugin/README.md, ../sm-pgstripe-plugin/go.mod, ../sm-oauthserver-plugin/console/prune.go, ../sm-oauthserver-plugin/console/rotate_keys.go, ../sm-oauthserver-plugin/console/import_winter.go, ../sm-oauthserver-plugin/commands_documented_test.go, ../sm-oauthserver-plugin/README.md, ../sm-oauthserver-plugin/go.mod, ../sm-journal-plugin/console/export_posts.go, ../sm-journal-plugin/console/import_posts.go, ../sm-journal-plugin/commands_documented_test.go, ../sm-journal-plugin/README.md, ../sm-journal-plugin/go.mod, ../sm-sitemap-plugin/console/import.go, ../sm-sitemap-plugin/console/import_winter.go, ../sm-sitemap-plugin/commands_documented_test.go, ../sm-sitemap-plugin/README.md, ../sm-sitemap-plugin/go.mod, ../sm-faq-plugin/console/import_winter.go, ../sm-faq-plugin/commands_documented_test.go, ../sm-faq-plugin/README.md, ../sm-faq-plugin/go.mod, ../sm-menu-plugin/console/import_winter.go, ../sm-menu-plugin/commands_documented_test.go, ../sm-menu-plugin/README.md, ../sm-menu-plugin/go.mod (plus each repository's go.sum and the non-console files listed in this plan's frontmatter that log through the process default logger) + 19.8-03-SUMMARY.md draft (`## Handoff` and the Task 6 reply; segment B starts here in a fresh executor context, see Execution segments), CLAUDE.md (Commits and concurrency, changelog rule), ../sm-summercmsio-app/DEPLOY.md ("Posting to the changelog"), ../sm-summercmsio-app/scripts/changelog-post.py (usage header), ../sm-sitemap-plugin/console/import.go (analog), ../sm-paymentgateway-plugin/schedule.go, each repository's console/*.go, go.mod, go.work and README.md, this plan's checklist table (shared plugin rows) + Per D-20, D-21, D-22, D-24 and D-26, under the recorded reply. Segment B start (fresh context): work from the handoff, not from memory of Tasks 1-5; first check that every hash it lists resolves (`git -C cat-file -e `) and that each repository's HEAD, uncommitted paths and origin/master..master count match what it recorded; a mismatch stops the task and is reported. Before every push: `git fetch origin`, `git status` shows no uncommitted change to this phase's paths, and `git log --oneline origin/master..master` lists only commits the user approved (summercms.go: the commits named at the checkpoint; a plugin: its one sweep commit); anything else stops the task. Never force-push; never deploy. + +(1) Framework: if the reply's tag is not v0.4.0, rename the upgrading section heading to that version and commit (`docs: upgrading notes for vX.Y.Z`). `go vet ./...` and `go test -short ./...` green; annotated tag vX.Y.Z on master HEAD with a one-paragraph message (Artisan-style console, diary logging with daily files, startup summaries and reports, request log by route pattern, plus what else the commits since the previous tag contain); `git push origin master`, then `git push origin vX.Y.Z`. From ../sm-summercmsio-app run `scripts/changelog-post.py source vX.Y.Z`, write user-facing notes (## Highlights, ## Added, ## Changed, ## Fixed; no H1, no planning IDs, no quick codes; Changed lists the upgrading items) to a file in the session scratchpad, then `scripts/changelog-post.py publish vX.Y.Z --excerpt ""`. If the token file is missing or refused, ask the user; never print, copy or mint a token. + +(2) For each repository in the order sm-paymentgateway-plugin, sm-pgstripe-plugin, sm-oauthserver-plugin, sm-journal-plugin, sm-sitemap-plugin, sm-faq-plugin, sm-menu-plugin, one commit containing all of: `go get git.golem15.com/golem15/summercms@vX.Y.Z` and `go mod tidy` (existing replace and go.work setups kept); each command per its checklist row: description in imperative sentence case without a trailing period, Long (what it does, when to run it, scheduling and flag interplay, for importers the meaning of their existing --force and --dry-run), two or three Examples, the result line, the added flags (none of the existing flags renamed or re-meant); a package-level `diary.For("")` logger (paymentgateway, pgstripe, oauthserver, journal, sitemap, faq, menu) replacing every non-test use of the process default logger in the repository's files (frontmatter list); `commands_documented_test.go` with `TestCommandsDocumented` that builds the plugin's commands with an empty app, calls bonfire.CheckDocs and logs each problem with t.Logf without failing (D-17 warn-only); README CLI commands section with each command, its flags and an example, and Dependencies naming SummerCMS vX.Y.Z. Checks before committing: `go vet ./...`, `go test -short ./...`, `GOWORK=off go build ./...` (proves the tag carries the API) and the warn-only test runs. Commit `feat: console help, examples and named loggers (SummerCMS vX.Y.Z)`, then `git push origin master`. + +Record in the SUMMARY's handoff, before Task 8 starts (so Task 8 can resume from the handoff alone if segment B is interrupted): the tag and its pushed range, the changelog post URL, each plugin's commit hash and push result, each plugin's CheckDocs warnings (expected none). + + git ls-remote --exit-code --tags origin "$(git describe --tags --abbrev=0)" && for r in sm-paymentgateway-plugin sm-pgstripe-plugin sm-oauthserver-plugin sm-journal-plugin sm-sitemap-plugin sm-faq-plugin sm-menu-plugin; do go -C ../$r vet ./... && go -C ../$r test -count=1 -short ./... && go -C ../$r test -count=1 -short -run '^TestCommandsDocumented$' -v . 2>&1 | grep -q -- '^--- PASS: TestCommandsDocumented' && test -z "$(git -C ../$r log --oneline origin/master..master)" || exit 1; done + Non-zero exit: the framework tag is not on origin, a plugin fails vet or its short suite at the tag, its warn-only TestCommandsDocumented is missing or did not run, or the plugin still has unpushed commits. On "hold" the command is not run and the SUMMARY says so. + + + - On a release reply: `git ls-remote --exit-code --tags origin vX.Y.Z` exits 0 in summercms.go and the SUMMARY holds the changelog post URL. + - For each of the seven repositories: `grep -c 'git.golem15.com/golem15/summercms vX.Y.Z' go.mod` prints 1, `git log --oneline -1` is the sweep commit, and `git log --oneline origin/master..master` is empty. + - `for r in sm-paymentgateway-plugin sm-pgstripe-plugin sm-oauthserver-plugin sm-journal-plugin sm-sitemap-plugin sm-faq-plugin sm-menu-plugin; do C=$(git -C ../$r ls-files '*.go' | grep -v _test.go | sed "s|^|../$r/|" | xargs grep -lE 'slog\.Default\(\)|slog\.(Info|Warn|Error|Debug)(Context)?\(' 2>/dev/null); test -z "$C" || exit 1; done` exits 0. + - `for r in sm-paymentgateway-plugin sm-journal-plugin sm-sitemap-plugin; do B=$(git -C ../$r show HEAD~1:README.md 2>/dev/null | grep -oE '^\| `[a-z]+:[a-z:-]+`' | sort -u) && A=$(grep -oE '^\| `[a-z]+:[a-z:-]+`' ../$r/README.md | sort -u) && test -z "$(comm -23 <(printf '%s\n' "$B") <(printf '%s\n' "$A"))" || exit 1; done` exits 0 (no command name disappeared from a README table; adapt the pattern if a README lists commands differently and record it). + + The framework release is public with its changelog post, and seven shared plugins explain their commands, log through named components and are pinned to it, each in one pushed commit. + + + + Task 8: Under the Task 6 reply, sweep and push chat, chatchannels, chatvideo, forum, herald, golem, feedback and (when Phase 19.5 has released it) user, then bump the applications' pointers and regenerate their main.go in separate commits + Task 7 recorded the framework tag and seven pushed plugin commits; on "hold" this task records "release held" and does nothing else. For sm-user-plugin additionally: .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-04-SUMMARY.md exists and `git -C ../fonoteka.go/plugins/golem15/user status --porcelain` is empty and in sync with origin/master; otherwise the user plugin is deferred as step (1) describes (todo, g15office task, D-20 reported partial). + ../sm-chat-plugin/console/rewrite_context_types.go, ../sm-chatchannels-plugin/console/rewrite_context_types.go, ../sm-chatvideo-plugin/console/reconcile.go, ../sm-forum-plugin/console/rewrite_context_types.go, ../sm-herald-plugin/console/vapid_generate.go, ../fonoteka.go/plugins/golem15/golem/console/import_settings.go, ../fonoteka.go/plugins/golem15/feedback/console/import_settings.go, ../fonoteka.go/plugins/golem15/user/console/api_tokens.go, ../fonoteka.go/plugins/golem15/user/console/backfill_handles.go, ../fonoteka.go/plugins/golem15/user/console/require_password_change.go, each repository's commands_documented_test.go, README.md, go.mod and go.sum, the non-console files listed in this plan's frontmatter that log through the process default logger, and the application paths ../sm-demo-app/plugins/golem15/*, ../sm-demo-app/main.go, ../sm-bm-app/plugins/golem15/user, ../sm-bm-app/main.go, ../sm-grzybyfunkcjonalne-app/plugins/golem15/*, ../sm-grzybyfunkcjonalne-app/main.go, ../sm-summercmsio-app/plugins/golem15/*, ../sm-summercmsio-app/main.go, ../fonoteka.go/plugins/golem15/user, ../fonoteka.go/plugins/golem15/golem, ../fonoteka.go/plugins/golem15/feedback, ../fonoteka.go/main.go, and in summercms.go .planning/G15OFFICE.md (step 3) and, only when sm-user-plugin is deferred, .planning/todos/pending/user-plugin-console-sweep.md + 19.8-03-SUMMARY.md draft (`## Handoff` with the Task 6 reply and the Task 7 results), each repository's console files, plugin.go, go.mod and README.md, ../fonoteka.go/plugins/golem15/user/console_test.go, 19.8-RESEARCH.md ("Apps that need pointer bumps", Pitfalls 15 and 16), this plan's checklist table (shared plugin rows), CLAUDE.md (Sibling repositories), .planning/G15OFFICE.md + Per D-20, D-21, D-22, D-24 and D-26; same push rules as Task 7. + +(1) Plugins, one commit each, in the order sm-chat-plugin, sm-chatchannels-plugin, sm-forum-plugin, sm-chatvideo-plugin, sm-herald-plugin, sm-golem-plugin (../fonoteka.go/plugins/golem15/golem), sm-feedback-plugin (../fonoteka.go/plugins/golem15/feedback), then sm-user-plugin (../fonoteka.go/plugins/golem15/user) only when the precondition holds: the same content as Task 7 step (2) (require the tag, checklist row per command, description normalised, Long, Examples, result line, `--json` for herald:vapid-generate, user:token:create and user:token:list with the data on stdout kept under -q, progress for user:backfill-handles, planning IDs removed from user descriptions, a package-level `diary.For("")` logger across the repository, the warn-only TestCommandsDocumented, README CLI section and Dependencies), the same checks (vet, short tests, `GOWORK=off go build ./...`, user plugin through `go -C ../fonoteka.go test -short ./plugins/golem15/user/...` as in Phase 19.5), then push. The rewrite-context-types descriptions move "(idempotent)" into Long. When the sm-user-plugin precondition fails, do not touch that checkout; instead, in summercms.go, write `.planning/todos/pending/user-plugin-console-sweep.md` (frontmatter title "Sweep sm-user-plugin console commands to the Phase 19.8 bar", date, priority medium, area "sm-user-plugin console", `g15office: `; body: the observed reason for the deferral (19.5-04-SUMMARY.md missing, or the checkout dirty or out of sync), the user rows of this plan's Artisan checklist, the Task 7 step (2) content to apply, the framework tag to require, and the follow-up pointer bumps and regenerate commits in sm-demo-app, sm-bm-app, sm-grzybyfunkcjonalne-app, sm-summercmsio-app and fonoteka.go), create its g15office task (type Task, subtask of the Phase 19.8 milestone task, description ending `Local: .planning/todos/pending/user-plugin-console-sweep.md`) and write the ID into the frontmatter (if g15office is unavailable, `g15office: pending` and the SUMMARY says so), and commit the todo alone (`docs(19.8): defer the user plugin console sweep`). Record "D-20: partial (sm-user-plugin deferred, see .planning/todos/pending/user-plugin-console-sweep.md)" in the SUMMARY. + +(2) Applications, after every plugin they mount is pushed (mapping: user to sm-demo-app, sm-bm-app, sm-grzybyfunkcjonalne-app, sm-summercmsio-app and fonoteka.go; journal to sm-demo-app, sm-grzybyfunkcjonalne-app, sm-summercmsio-app; sitemap, menu, faq to sm-demo-app and sm-grzybyfunkcjonalne-app; paymentgateway, pgstripe, oauthserver to sm-demo-app; golem and feedback to sm-demo-app and fonoteka.go; chat, chatchannels, chatvideo, forum, herald to none; with the user plugin deferred, applications skip the user pointer, and sm-bm-app, whose only bumped plugin would be user, gets no commit in this task). In each application: fetch and check out each bumped submodule at its pushed commit (fonoteka.go's user, golem and feedback checkouts are the canonical ones and already there); `go build ./...`; commit only those submodule paths (`chore: bump for the console overhaul`); then `go run ../summercms.go/cmd/summer build` (or the installed summer) to regenerate main.go and build bin/, run `./bin/ list` (exit 0, no duplicate-name error, RESEARCH Pitfall 15) and `./bin/ help migrate` (exit 0), and commit only main.go (`build: regenerate main for SummerCMS vX.Y.Z`). Never stage other sessions' pointer changes (check `git diff --submodule` first). On "release" push each application and the Task 5 landing (vue-summercmsio-app, then sm-summercmsio-app) and deck (sm-demo-app) commits whose unpushed commits are all this phase's; on "release-keep-apps-local" leave them local and list them in the SUMMARY. + +(3) Post a short comment on the Phase 19.8 milestone task in g15office (find it by title; it is not yet in .planning/G15OFFICE.md, so add its ID there in a planning-doc commit when found) naming the release, the plugins and the applications; if the tool is unavailable, record that in the SUMMARY. + + for r in sm-chat-plugin sm-chatchannels-plugin sm-forum-plugin sm-chatvideo-plugin sm-herald-plugin fonoteka.go/plugins/golem15/golem fonoteka.go/plugins/golem15/feedback; do go -C ../$r vet ./... && go -C ../$r test -count=1 -short ./... && test -z "$(git -C ../$r log --oneline origin/master..master)" || exit 1; done && for a in sm-demo-app sm-bm-app sm-grzybyfunkcjonalne-app sm-summercmsio-app fonoteka.go; do go -C ../$a build ./... || exit 1; done + Non-zero exit: a plugin fails vet or tests at the tag or still has unpushed commits, or an application no longer builds with the bumped pointers. On "hold" the command is not run and the SUMMARY says so. + for a in sm-demo-app sm-bm-app sm-grzybyfunkcjonalne-app sm-summercmsio-app fonoteka.go; do if [ "$a" = sm-bm-app ] && test -f .planning/todos/pending/user-plugin-console-sweep.md; then continue; fi; B=$(awk '/^binary:/{print $2}' ../$a/summer.yaml) && (cd ../$a && ./bin/$B list >/dev/null && ./bin/$B help migrate >/dev/null) || exit 1; done + Non-zero exit: an application binary fails to boot its command root (for example a duplicate command name across plugins) or its help for migrate fails. + + + - For each swept plugin: `grep -c 'git.golem15.com/golem15/summercms vX.Y.Z' go.mod` prints 1 and `git log --oneline origin/master..master` is empty. When the sm-user-plugin precondition failed: the SUMMARY lists it as deferred with "D-20: partial", `test -f .planning/todos/pending/user-plugin-console-sweep.md` exits 0, and `grep -c '^g15office:' .planning/todos/pending/user-plugin-console-sweep.md` prints 1. + - `grep -c '(D-15)\|(D-21)' ../fonoteka.go/plugins/golem15/user/console/backfill_handles.go ../fonoteka.go/plugins/golem15/user/console/require_password_change.go | awk -F: '{s+=$2} END {print s}'` prints 0 when the user plugin was swept. + - In each application (except sm-bm-app when the user plugin was deferred): `git log --oneline -2` shows a pointer-bump commit and a regenerate commit as separate commits, and `grep -c 'bonfire.Execute(' main.go` prints 1. + - The SUMMARY lists every pushed repository with its pushed range, or the local commits on "release-keep-apps-local". + + Every shared plugin with commands is swept and pushed in one commit each, and every application runs the new console with the swept plugins, pinned through separate pointer-bump and regenerate commits. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| HTTP clients to the request log | paths carry share and invitation tokens; headers carry request ids of any length | +| config and environment to the startup summary and hints | the DSN and keys are in config; summaries and hints are printed to terminals and journald | +| operators over non-interactive SSH to destructive commands | deploys run migrate without a terminal | +| this machine to git.golem15.com and summercms.io | tags, pushes and the changelog post publish work, including other sessions' commits | +| framework to shared core plugins and applications | plugin APIs and command names are relied on by every application | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.8-13 | Information disclosure | surf request log (accesslog.go) | high | mitigate | records carry the route pattern only, never the path, query, headers or remote address (D-23); request_id capped at 128 bytes; acceptance greps for URL path access; TestServeLifecycle asserts the request path value is absent | +| T-19.8-14 | Information disclosure | startup summary (summary.go) | high | mitigate | database shown as host, port and name from pgconn.ParseConfig, never user, password or DSN; driver names only for mail, cache, realtime and search; TestServeSummarySmoke asserts the password and user never appear | +| T-19.8-15 | Denial of service | production confirmation (lagoon, conga) | high | mitigate | ConfirmProceed asks only on an interactive terminal; non-TTY deploys and scheduler Call runs proceed as today (D-28); --force skips; existing --force meanings untouched | +| T-19.8-16 | Tampering | shared plugin contracts | medium | mitigate | one additive commit per plugin, names and flags unchanged (README command-table check), GOWORK=off build at the tag, short suites green; the plan 04 gate adds an exported-symbol additive check | +| T-19.8-17 | Repudiation | release publication | medium | mitigate | blocking checkpoint (Task 6) lists origin..master before any push; pushes only after the recorded reply; never force-push or deploy | +| T-19.8-18 | Information disclosure | changelog token | medium | mitigate | scripts/changelog-post.py reads the token file itself; the token is never printed, copied or minted; a missing token stops and asks the user | +| T-19.8-19 | Information disclosure | hints (bonfire errors.go) | low | accept | Try lines show local relative paths and command names an operator already knows; messages still pass through sunscreen.Scrub | +| T-19.8-20 | Denial of service | worker shutdown (conga) | low | mitigate | StopAndCancel after the deadline as today; in-flight jobs reported as handed back only after confirming River's retry behaviour | +| T-19.8-SC | Tampering | module requires in plugins | medium | mitigate | plugins require only the framework tag produced in Task 7; no new third-party module is added by this plan | + + + +- Task 1: serve smokes and TestServeLifecycle (Docker); request log by pattern; docs checker. +- Task 2: conga, dev and summer suites; dev runs serve. +- Task 3: whole short suite, TestCommandsDocumented over application commands, TestGreeterDocumented. +- Task 4: TestCommandsDocumented over all framework commands; every listed command has examples. +- Task 5: landing node tests, terminal data, roadmap tests and snapshot check. +- Task 6: reply recorded; nothing published before it. +- Tasks 7-8: tag on origin, changelog post, each plugin's suite at the tag and pushed, each application builds and boots list and help. When sm-user-plugin was deferred, D-20 is reported partial, naming .planning/todos/pending/user-plugin-console-sweep.md and its g15office ID, never as passed. +- Segments: the `## Handoff` section exists in the SUMMARY draft before the Task 6 checkpoint returns, and Tasks 7-8 ran from it in a fresh context. +- On "hold": the SUMMARY notes that the local deck and landing commits describe an unreleased console. +- Feedback sampling: after each framework commit `go test -short ./...` and `go -C examples/hello test ./...`; after Task 4, `scripts`-free full run `go vet ./... && go test ./...` on a Docker host. + + + +- D-04..D-07, D-29: summaries and reports for serve, queue:work, schedule:run and dev, exit 0 on graceful stop. +- D-11, D-23: request log by route pattern, on by default. +- D-17, D-21, D-22: every framework command documented and gated; the checklist table implemented; result lines. +- D-18, D-19: hints for the known failures; Reported for self-reporting commands. +- D-20, D-24, D-26: release in order after the go-ahead; one commit per plugin; separate application commits. +- D-25: summer dev serves. +- SC-5: docs, READMEs, landing and deck updated. + + + +Create `.planning/phases/19.8-console-commands-overhaul/19.8-03-SUMMARY.md` when done, with the commits in order per repository, the Task 6 reply verbatim, the tag, the changelog post URL, the pushed ranges (or local commits), Docker tests run or NOT RUN, each plugin's CheckDocs warnings, whether sm-user-plugin was swept or deferred (when deferred: the todo path, its g15office ID and "D-20: partial"), the `## Handoff` section (kept in the final SUMMARY), on "hold" the note that the local deck and landing commits describe an unreleased console, the g15office result, and any checklist row that changed during execution with its reason. + diff --git a/.planning/phases/19.8-console-commands-overhaul/19.8-04-PLAN.md b/.planning/phases/19.8-console-commands-overhaul/19.8-04-PLAN.md new file mode 100644 index 00000000..df072db0 --- /dev/null +++ b/.planning/phases/19.8-console-commands-overhaul/19.8-04-PLAN.md @@ -0,0 +1,353 @@ +--- +phase: 19.8-console-commands-overhaul +plan: 04 +type: execute +wave: 4 +depends_on: ["19.8-01", "19.8-02", "19.8-03"] +files_modified: + - scripts/check-phase19.8.sh + - modules/diary/diary_test.go + - modules/diary/config_test.go + - modules/diary/daily_test.go + - modules/diary/console_test.go + - modules/diary/slogtest_test.go + - modules/sunscreen/sunscreen_test.go + - modules/bonfire/globals_test.go + - modules/bonfire/root_test.go + - modules/bonfire/golden_test.go + - modules/bonfire/errors_test.go + - modules/bonfire/docs_test.go + - modules/bonfire/widgets_test.go + - modules/bonfire/live_test.go + - modules/bonfire/version_test.go + - modules/bonfire/testdata/list.golden + - modules/bonfire/testdata/list_all.golden + - modules/bonfire/testdata/list.json.golden + - modules/bonfire/testdata/help_token_create.golden + - modules/bonfire/testdata/help_passthrough.golden + - modules/bonfire/testdata/error_hint.golden + - modules/bonfire/testdata/error_unknown_command.golden + - modules/bonfire/testdata/error_chain_verbose.golden + - modules/bonfire/testdata/list_styled.golden + - internal/docsite/check_commands_test.go + - modules/surf/summary_test.go + - modules/surf/accesslog_test.go + - modules/conga/stats_test.go + - modules/conga/logger_test.go + - modules/lagoon/pending_test.go + - modules/lagoon/commands_flags_test.go + - modules/lagoon/logger_test.go + - modules/cabana/token_json_test.go + - modules/flare/commands_test.go + - internal/dev/watch_test.go + - cmd/summer/runtime_test.go + - .planning/phases/19.8-console-commands-overhaul/19.8-04-SUMMARY.md +autonomous: false +requirements: [SC-5, D-03, D-08, D-09, D-10, D-12, D-13, D-14, D-15, D-16, D-17, D-18, D-19, D-21, D-23, D-25, D-27, D-28, D-29] +user_setup: [] + +estimate: + tokens: 230000 + raw_tokens: 230000 + tasks: 6 + confidence: low + +must_haves: + truths: + - "scripts/check-phase19.8.sh runs stages self-test, go, hello, docs, plugins, apps, landing and docker; a stage fails on a failing command or on a go test -json run that fails, skips a required test, matches zero tests, prints 'no tests to run' or misses a required name; --self-test proves those detectors on planted output; --all is green before /msd-verify-work (docker NOT RUN only when no daemon answers and PHASE198_REQUIRE_DOCKER is unset)." + - "D-08, D-09, D-10, D-12: diary has unit tests for every config rule and its key-naming error, env strings, component floors and verbosity, quiet on the console only, daily rotation across midnight with an injected clock, retention deleting only base-YYYY-MM-DD+ext files, 0640 files, an unwritable directory, path confinement, redaction on every driver, control-character stripping, loggers created before Setup, Reset, HideOnTTY and concurrent writes under -race; the console handler and the JSON pipeline pass testing/slogtest." + - "D-13..D-18: bonfire golden files pin the list (plain and --all), the JSON listing, per-command help (value, bare and repeatable flags with defaults, examples, long help, passthrough) and the error blocks (hint with {bin}, unknown command with suggestion, -v chain), rendered with the color profile forced off plus one styled list golden; exit codes 0, 1 and 2, Reported silence, duplicate and reserved-flag errors, PeekGlobals and the cobra flags agree, and Call never runs OnGlobals." + - "D-03, D-27, D-28: a widget matrix test covers Spinner, Progress, Process, Table and every prompt across terminal, not a terminal, -q, --no-ansi and -n; the live models are tested through Update and View without a terminal; ConfirmProceed never reads input off a terminal; Secret with -n reads piped stdin and fails with a hint otherwise." + - "D-19, D-21, D-23, D-25, D-29: tests cover the wrong-directory hint, migrate and rollback --pretend and the production confirmation rule (non-TTY proceeds), --json outputs (migrate:status, route:list, admin tokens, key:generate) and -q data, the request log's route pattern, request_id cap and default, Worker.Stats, PendingMigrations, the serve summary and shutdown report builders, summer dev starting serve with forwarded flags, and graceful shutdown returning nil." + - "D-17: the gate's plugins stage runs each shared plugin's warn-only TestCommandsDocumented and an additive check (no exported func, method or type and no command Name or flag Name literal removed since the commit before its sweep commit); the apps stage builds each application and boots bin/ list." + - "A bug found by these tests in code that is already in the plan 03 release is fixed in its own commit and reaches the applications only after a recorded go-ahead (patch tag, plugin requires, pointer bumps)." + artifacts: + - path: "scripts/check-phase19.8.sh" + provides: "fail-closed multi-repository phase gate with --self-test" + contains: "--self-test" + - path: "modules/diary/slogtest_test.go" + provides: "testing/slogtest conformance for the console handler and the pipeline" + contains: "slogtest.TestHandler" + - path: "modules/bonfire/golden_test.go" + provides: "golden tests with an -update flag" + contains: "flag.Bool(\"update\"" + - path: "modules/bonfire/widgets_test.go" + provides: "widget degradation matrix" + contains: "TestWidgetMatrix" + key_links: + - from: "scripts/check-phase19.8.sh" + to: "modules/diary/slogtest_test.go" + via: "required test names in the go stage" + pattern: "TestSlogtest" + - from: "modules/bonfire/golden_test.go" + to: "modules/bonfire/testdata/list.golden" + via: "assertGolden compares rendered output" + pattern: "testdata" + prohibitions: + - statement: "No test is skipped, deleted or weakened to make the gate pass; a failing behaviour is fixed in code or reported." + status: unresolved + verification: judgment + - statement: "Golden files and test fixtures contain no real token, key, password or DSN credential." + status: unresolved + verification: judgment + - statement: "Nothing is pushed or tagged without the Task 5 reply." + status: unresolved + verification: judgment +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format; no story is invented, see plan 01): console commands reach the Artisan bar; a user sees what a command did, can ask for more or less of it, and learns from `--help` how to use it. + +This plan's slice (plan 4 of 4, wave 4; project rule: unit tests are the last plan of every phase): a fail-closed phase gate first (the tracer), then unit and golden tests for diary, bonfire and every framework change of plans 1-3, wired into the gate together with the plugin, application, landing and Docker stages. The shared plugins already carry their tests in their single sweep commits (D-20 allows one commit per plugin repository), so this plan runs them from the gate rather than adding commits there. A conditional go-ahead covers fixes to code that plan 3 already released. + +## Spec-less probe fallback + +Spec-less probe fallback skipped: no requirement IDs (phase_req_ids null) and no SPEC.md; visible choice. Tests trace to SC-1..SC-5 and the D-IDs in `requirements`. + +## Decisions taken in this plan (recorded assumptions and Claude's discretion) + +- Goldens render with the color profile forced to NoTTY (no escape codes) through the injectable console constructor, plus one styled golden (`list_styled.golden`, ANSI256) so a palette change is visible in review. The `-update` flag and `assertGolden` helper copy internal/build/new_golden_test.go. +- The console handler is checked with testing/slogtest through a small parser of its `key=value` attribute output (dotted group keys become nested maps); the full pipeline (component handler, sunscreen, stack) is checked through its JSON output. +- No teatest dependency: models are tested by calling Update and View; one end-to-end run uses `tea.WithInput(nil)`, a buffer output and a fixed window size (RESEARCH Testing techniques). +- The plugin additive check finds each plugin's sweep commit by its subject ("console help, examples and named loggers (SummerCMS v") and compares against its parent, so no base hash has to be typed; sm-user-plugin is skipped with a NOT SWEPT line when plan 3 deferred it. +- Post-release fixes follow the Phase 19.5 precedent: Task 5 is presented only when Tasks 2-4 changed non-test code that is in the plan 3 tag. + +## Artifacts this phase produces (this plan) + +- `scripts/check-phase19.8.sh` with stages `self-test`, `go`, `hello`, `docs`, `plugins`, `apps`, `landing`, `docker` and `--all`; environment overrides `PHASE198_ROOT`, `PHASE198_REQUIRE_DOCKER`, `PLUGINS_ROOT`, `FONOTEKA`, `DEMO_APP`, `BM_APP`, `GRZYBY_APP`, `SUMMERCMSIO_APP`. +- Test files (names under each task): diary `diary_test.go`, `config_test.go`, `daily_test.go`, `console_test.go`, `slogtest_test.go`; bonfire `globals_test.go`, `root_test.go`, `golden_test.go`, `errors_test.go`, `docs_test.go`, `widgets_test.go`, `live_test.go`, `version_test.go` and goldens under `modules/bonfire/testdata/`; docsite `check_commands_test.go`; surf `summary_test.go`; conga `stats_test.go`, `logger_test.go`; lagoon `pending_test.go`, `commands_flags_test.go`, `logger_test.go`; cabana `token_json_test.go`; flare `commands_test.go`; internal/dev `watch_test.go`; cmd/summer `runtime_test.go`. + + +Prove the phase: a gate that cannot pass on skipped or missing tests, unit and golden tests for every new behaviour in diary, bonfire and the swept framework commands, the shared plugins' and applications' checks run from one script, and a controlled path for fixing anything already released. + +Purpose: SC-5 (unit tests in the last plan) and the D-IDs listed in requirements. +Output: the gate script, test commits per module, a green `--all` run recorded in the SUMMARY, and, only if needed, the post-release go-ahead and its publication. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/19.8-console-commands-overhaul/19.8-CONTEXT.md +@.planning/phases/19.8-console-commands-overhaul/19.8-RESEARCH.md +@.planning/phases/19.8-console-commands-overhaul/19.8-01-SUMMARY.md +@.planning/phases/19.8-console-commands-overhaul/19.8-02-SUMMARY.md +@.planning/phases/19.8-console-commands-overhaul/19.8-03-SUMMARY.md + +Repos and concurrency: tests and the gate are written in summercms.go only; the gate reads the sibling repositories and builds the applications without committing there (a build that rewrites an application's main.go must leave no diff, because plan 3 committed the regenerated main.go; if it does, report). Check `git status --porcelain` on each task's paths first. Stage by explicit path; no co-author tags; test commits separate from any fix commit; `go vet ./...`, `go test -short ./...` and `go -C examples/hello test ./...` green at every commit. Docker tests run on a Docker host, through `sudo -u golem` if the socket is denied; a Docker test that could not run is NOT RUN in the SUMMARY. + + +- Gate pattern: scripts/check-phase19.4.sh and scripts/check-phase19.5.sh (stage functions, required-name arrays run with `-run '^(a|b)$'`, go test -json parsing that fails on fail, skip, zero tests or a missing name, `--self-test` with planted output, `unset FORCE_COLOR`, `export NO_COLOR=1`, app names split as `"fono""teka.go"` so the docs checker never sees them whole). +- Golden helper to copy: internal/build/new_golden_test.go:13 (`var update = flag.Bool("update", false, ...)`) and :76-96 (`assertGolden`, `firstDiff`). +- Console test seam: modules/bonfire/output.go `newConsole(in, out, errW, lookup, tty, interactive *bool)` (plan 02 adds quiet, noInteraction and color mode fields); bonfire `DetectProfile`, `Globals`, `PeekGlobals`, `Execute`, `RenderError`, `Describe`, `CheckDocs`, `ConfirmProceed`, `Process`, `PrintAbove`, `BuildVersion` (plan 02 SUMMARY lists the final names). +- diary: `Setup`, `Bootstrap`, `For`, `SetVerbosity`, `Verbosity`, `Reset`, `Close`, `ParseConfig`, `DefaultConfig`, `NewConsoleHandler`, `HideOnTTY`, `Options.Now` (plan 01 and 03 SUMMARYs). +- Smoke tests already present (keep them; the gate requires them): TestDiaryTracerSmoke, TestConfigEnvSmoke, TestDailyDegradesSmoke, TestRiverLoggerUsesDiarySmoke, TestGormLoggerUsesDiarySmoke, TestListTracerSmoke, TestHelpSmoke, TestListJSONSmoke, TestErrorRenderSmoke, TestWidgetMatrixSmoke, TestServeSummarySmoke, TestShutdownReportSmoke, TestServeLifecycle (Docker), TestCommandsDocumented, TestGreeterDocumented (examples/hello), TestBuiltBinaryListsCommands (examples/hello). +- Plugin repositories and application paths: plan 03 frontmatter and SUMMARY (which plugins were swept, the tag, which applications were bumped). + + + + + + + Task 1: Tracer: scripts/check-phase19.8.sh runs the go stage with plans 1-3's smoke tests by name and fails when one is missing, skipped or red, proven by --self-test + scripts/check-phase19.8.sh + scripts/check-phase19.5.sh (whole file: structure, detectors, self-test), scripts/check-phase19.4.sh (stages, NOT RUN rules, additive check), 19.8-01-SUMMARY.md, 19.8-02-SUMMARY.md, 19.8-03-SUMMARY.md (final test names), 19.8-RESEARCH.md (Validation Architecture) + Create scripts/check-phase19.8.sh (bash, `set -euo pipefail`, `unset FORCE_COLOR`, `export NO_COLOR=1`) in the 19.5 gate's structure with a header comment describing every stage. Roots: `ROOT` from PHASE198_ROOT or the script's parent; `PLUGINS_ROOT` default `$ROOT/..`; `FONOTEKA` default `$ROOT/../fono""teka.go`; application defaults next to ROOT. Stages in this task: `self-test` (planted go test -json streams for a pass, a fail, a skip, a subtest-only pass, zero tests and "no tests to run" must be classified correctly, and a missing required name must fail) and `go` (`go vet ./...`, `go test -count=1 -short ./...`, then required names per package with `go test -count=1 -short -json -run '^(...)$'`: diary TestDiaryTracerSmoke TestConfigEnvSmoke TestDailyDegradesSmoke; bonfire TestListTracerSmoke TestHelpSmoke TestListJSONSmoke TestErrorRenderSmoke TestWidgetMatrixSmoke; conga TestRiverLoggerUsesDiarySmoke; lagoon TestGormLoggerUsesDiarySmoke; surf TestServeSummarySmoke TestShutdownReportSmoke TestAccessLogSmoke; cmd/summer TestCommandsDocumented TestDocsTree; then `go test -count=1 -race -short ./modules/diary/... ./modules/bonfire/...`). Stubs that print "stage : not implemented" and exit 1 for hello, docs, plugins, apps, landing and docker until Task 4 (so `--all` cannot pass early). `--all` runs self-test, go, hello, docs, plugins, apps, landing, then docker (NOT RUN docker when `docker info` fails, a failure with PHASE198_REQUIRE_DOCKER=1). Make it executable (git mode 100755). Commit: `test(19.8): phase gate with the smoke tests by name`. + + bash scripts/check-phase19.8.sh --self-test && bash scripts/check-phase19.8.sh go + Non-zero exit: a detector misclassifies planted output in --self-test, or a required smoke test is missing, skipped or failing, or vet, the short suite or the race run fail. + + + - `test -x scripts/check-phase19.8.sh` exits 0 and `M=$(git ls-files -s scripts/check-phase19.8.sh) && test "${M:0:6}" = 100755` exits 0. + - `grep -c 'NO_COLOR=1' scripts/check-phase19.8.sh` prints at least 1 and `grep -cE '"?fono""teka' scripts/check-phase19.8.sh` prints at least 1. + - `bash scripts/check-phase19.8.sh --all; test $? -ne 0` exits 0 at this point (the unimplemented stages keep --all red). + - `grep -cE 'TestDiaryTracerSmoke|TestListTracerSmoke|TestServeSummarySmoke' scripts/check-phase19.8.sh` prints at least 3. + + One command proves the phase's smoke tests ran and passed by name, and the gate cannot be fooled by a skip, a typo or an empty run. + + + + Task 2: diary, sunscreen and the River and GORM bridges have unit tests: config rules, env strings, floors, verbosity, quiet, daily rotation and retention, redaction on every driver, slogtest conformance, concurrency + modules/diary/diary_test.go, modules/diary/config_test.go, modules/diary/daily_test.go, modules/diary/console_test.go, modules/diary/slogtest_test.go, modules/sunscreen/sunscreen_test.go, modules/conga/logger_test.go, modules/lagoon/logger_test.go, scripts/check-phase19.8.sh + + - TestConfigRules (table): every invalid driver, format, level, days, stack child, stack cycle and escaping path returns an error naming its exact key; valid YAML lists and comma strings parse alike. + - TestConfigEnvStrings: SUMMER_LOG__ overrides as strings overlay defaults key by key; TestDefaultConfig equals the D-08 example with components river warn. + - TestComponentFloors and TestVerbosityLevels: components[name] beats log.level; verbosity 1 lets INFO through for a warn component, 2 lets DEBUG through, 3 also sets AddSource; TestQuietConsoleOnly: quiet raises the console to WARN while a daily INFO record is still written. + - TestDailyRotation (clock 23:59:59 to 00:00:01 gives two files), TestDailyRetention (days 2 deletes only matching names older than 2 days, keeps other files), TestDailyPermissions (0640 file, 0750 dir), TestDailyUnwritable (one warning, later writes succeed silently), TestDailyLazyOpen (no file without a record), TestPathConfinement (relative escape rejected, absolute accepted). + - TestRedactionEveryDriver: console, stderr, daily and stack outputs show [REDACTED] for password, token and api_key attributes and a scrubbed Bearer string in the message; null writes nothing. + - TestConsoleStripsControlChars, TestConsoleComponentPrefix (fixed width), TestConsoleStyledOnlyOnTerminal (NoTTY profile has no escape codes, ANSI256 has them). + - TestForBeforeSetup, TestResetRestoresDefault, TestNoRecursionWithoutState, TestHideOnTTY (skipped by the styled console only), TestConcurrentWrites (100 goroutines, -race, every line intact). + - TestSlogtestConsoleHandler and TestSlogtestPipeline pass testing/slogtest.TestHandler. + - TestInstallDefaultUnchanged (sunscreen): InstallDefault still installs a redacting TextHandler on the given writer. + - TestRiverLoggerComponent and TestRiverLoggerPublishedOverride (conga); TestGormLoggerLevels (lagoon): Warn by default, Info at verbosity 3, record-not-found ignored, parameterized SQL in the trace. + + modules/diary/*.go (all, from plans 01-03), modules/diary/tracer_smoke_test.go, modules/diary/config_smoke_test.go, modules/sunscreen/sunscreen_test.go, modules/conga/logger_smoke_test.go, modules/lagoon/logger_smoke_test.go, scripts/check-phase19.8.sh, Go source of testing/slogtest (TestHandler and its results contract), 19.8-RESEARCH.md (Pitfalls 4-8, Security Domain) + Write the tests in the behavior list (red first against any behaviour that turns out wrong; fix the code in a separate `fix(diary): ...` commit when a test exposes a bug, and record it in the SUMMARY's "Post-release fixes" table when the code is already in the plan 03 tag). The slogtest console parser reads one line per record: the fields after the message as key=value pairs, dotted keys nested into maps, the time and level keys mapped to slog.TimeKey and slog.LevelKey, the message to slog.MessageKey. Concurrency and time come from Options.Now and goroutines with a WaitGroup; no sleeps as synchronisation. Add every new test name to the gate's go stage required lists (diary, sunscreen, conga, lagoon). Commits: `test(diary): config, channels, rotation, redaction and slogtest` and `test(conga,lagoon): River and GORM bridges`. + + go test -count=1 -short -race ./modules/diary/... ./modules/sunscreen/... && go test -count=1 -short -run '^(TestSlogtestConsoleHandler|TestSlogtestPipeline|TestRedactionEveryDriver|TestDailyRotation|TestDailyRetention|TestConfigRules)$' -v ./modules/diary 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 6 && go test -count=1 -short ./modules/conga/... ./modules/lagoon/... && bash scripts/check-phase19.8.sh go + Non-zero exit (including a race report), or the count of top-level "--- PASS: Test" lines is not 6 (slogtest, redaction, rotation, retention or the config table failed or did not run), or the gate's go stage misses a newly required name. + + + - `grep -c 'slogtest.TestHandler' modules/diary/slogtest_test.go` prints at least 2. + - `grep -cE 'func Test(ConfigRules|ConfigEnvStrings|ComponentFloors|VerbosityLevels|QuietConsoleOnly|DailyRotation|DailyRetention|DailyPermissions|DailyUnwritable|DailyLazyOpen|PathConfinement|RedactionEveryDriver|ConsoleStripsControlChars|ForBeforeSetup|ResetRestoresDefault|HideOnTTY|ConcurrentWrites)\(' modules/diary/*_test.go | awk -F: '{s+=$2} END {print s}'` prints 17. + - `grep -cE 'TestSlogtestConsoleHandler|TestRedactionEveryDriver|TestGormLoggerLevels' scripts/check-phase19.8.sh` prints at least 3. + - `grep -rnE 'time\.Sleep' modules/diary/*_test.go | wc -l` prints 0. + + Every logging rule of D-08, D-09, D-10 and D-12 is pinned by a test that runs in the gate, including slogtest conformance and race-free concurrent writes. + + + + Task 3: bonfire has golden and unit tests for list, help, the JSON listing, error blocks, exit codes, global flags, duplicates, the widget matrix, the live models and prompts; the docs checker knows the global flags + modules/bonfire/globals_test.go, modules/bonfire/root_test.go, modules/bonfire/golden_test.go, modules/bonfire/errors_test.go, modules/bonfire/docs_test.go, modules/bonfire/widgets_test.go, modules/bonfire/live_test.go, modules/bonfire/version_test.go, modules/bonfire/testdata/list.golden, modules/bonfire/testdata/list_all.golden, modules/bonfire/testdata/list.json.golden, modules/bonfire/testdata/help_token_create.golden, modules/bonfire/testdata/help_passthrough.golden, modules/bonfire/testdata/error_hint.golden, modules/bonfire/testdata/error_unknown_command.golden, modules/bonfire/testdata/error_chain_verbose.golden, modules/bonfire/testdata/list_styled.golden, internal/docsite/check_commands_test.go, scripts/check-phase19.8.sh + + - TestPeekGlobals (table): -v, -vv, -vvv, --verbose twice, -q implies -n, --ansi then --no-ansi (last wins), --env X and --env=X, stop at --, unknown flags ignored, never an error; TestGlobalsArgs round-trips; TestGlobalFlagsAgree: the OnGlobals hook sees the same Globals as PeekGlobals for each table row. + - TestReservedFlags (each reserved name and shorthand returns an error naming command and flag, no panic), TestDuplicateNames (two commands, and a command named list or help), TestCallNoGlobalsHook. + - TestListGolden, TestListAllGolden, TestListJSONGolden, TestListStyledGolden; TestHelpGolden (admin:token:create shape from D-14 with value, bare and repeatable flags, defaults "*" and 90, long help, two examples) and TestHelpPassthroughGolden; TestHelpEqualsHelpFlag. + - TestErrorGolden (hint with {bin} as bin/acme, unknown command with suggestion, chain at -v), TestExitCodes (0 success, 1 failure, 2 unknown command, unknown flag and wrong argument count), TestReportedSilent, TestErrorScrubbed (a Bearer token and a password attribute in the message), TestWrongDirectoryHint (temp tree: app root with summer.yaml, run from plugins/ without config/ gives `cd ..; bin/acme migrate`; summer gets no such hint). + - TestCheckDocs: each missing item is reported, results sorted, a complete command reports nothing. + - TestVersionHeader: tagged dep, replaced dep (local), go.work devel and the summer tool's own build (through an injectable build-info reader). + - TestWidgetMatrix: Spinner, Progress, Process, Table, Info, Success, Warning, Error, Println across terminal, not a terminal, -q, --no-ansi and -n; not-a-terminal output byte-identical to the pre-phase format. + - TestLiveModels: spinner, progress (Current and Fail), process (pending, running, done, failed rows) and selector (move, enter, confirmation screen, esc back, ctrl+c abort) through Update and View; one end-to-end program run with tea.WithInput(nil) into a buffer. + - TestPromptsNoInteraction (defaults without reading stdin), TestSecretWithNoInteraction (piped stdin read; terminal-like stdin returns a HintError), TestConfirmProceed (non-interactive returns true without reading; interactive asks), TestPrintAbove (false without a program; a running program receives the line). + - TestCommandWordGlobals (docsite): `bin/acme -v serve`, `summer -q migrate`, `bin/acme --env staging migrate`, `bin/acme --env=staging migrate`, `summer list`, `bin/acme help migrate` resolve to the right command word. + + modules/bonfire/*.go (all, from plans 01-03), modules/bonfire/*_smoke_test.go, modules/bonfire/output_test.go, modules/bonfire/prompts_test.go, internal/build/new_golden_test.go (:13, :76-96), internal/docsite/check_commands.go, existing internal/docsite tests for commandWord, scripts/check-phase19.8.sh, 19.8-CONTEXT.md (D-13, D-14 and D-18 previews) + Write the tests in the behavior list. golden_test.go copies the `update` flag and `assertGolden` (path `testdata/.golden`, first-difference message, "run go test -run -update to create it"). Goldens use a fixed Version (`SummerCMS acme v1.4.0 (framework v0.3.2)`) and the NoTTY profile except list_styled.golden (ANSI256). Create the goldens with `-update`, then read each one and confirm it matches the D-13, D-14 and D-18 previews in substance (sections, order, alignment); record any intentional difference in the SUMMARY. A behaviour that a test shows to be wrong is fixed in a separate `fix(bonfire): ...` commit (Post-release fixes table when already tagged). Add every new test name to the gate's go stage (bonfire and docsite lists). Commits: `test(bonfire): goldens for list, help and errors`, `test(bonfire): flags, exit codes and the widget matrix`, `test(docsite): command word with global flags`. + + go test -count=1 -short -race ./modules/bonfire/... ./internal/docsite/... && go test -count=1 -short -run '^(TestListGolden|TestHelpGolden|TestErrorGolden|TestExitCodes|TestWidgetMatrix|TestLiveModels|TestPeekGlobals|TestWrongDirectoryHint)$' -v ./modules/bonfire 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 8 && go test -count=1 -short -run '^TestCommandWordGlobals$' -v ./internal/docsite 2>&1 | grep -q -- '^--- PASS: TestCommandWordGlobals' && bash scripts/check-phase19.8.sh go + Non-zero exit (including a race report or a golden mismatch), the count of top-level "--- PASS: Test" lines is not 8, no "--- PASS: TestCommandWordGlobals", or the gate's go stage misses a newly required name. + + + - `ls modules/bonfire/testdata/*.golden | wc -l` prints 9. + - `grep -c 'flag.Bool("update"' modules/bonfire/golden_test.go` prints 1. + - `grep -c 'Available commands:' modules/bonfire/testdata/list.golden` prints 1, `grep -c 'Examples:' modules/bonfire/testdata/help_token_create.golden` prints 1 and `grep -c 'Try:' modules/bonfire/testdata/error_hint.golden` prints 1. + - `grep -c $'\x1b\\[' modules/bonfire/testdata/list.golden` prints 0 and `grep -c $'\x1b\\[' modules/bonfire/testdata/list_styled.golden` prints at least 1. + - `grep -rnEi 'eyJ[a-zA-Z0-9_-]{10,}|sk-[A-Za-z0-9]{20,}|BEGIN (RSA |EC )?PRIVATE KEY' modules/bonfire/testdata | wc -l` prints 0. + + The approved list, help and error layouts are pinned by goldens, and every flag, exit-code, widget and prompt rule of D-03, D-13..D-18, D-27 and D-28 is a test in the gate. + + + + Task 4: The swept framework commands have unit tests (request log, summaries, reports, stats, pending migrations, --pretend, confirmation, --json, -q data, Reported, dev runs serve), the gate's remaining stages run, and --all is green + modules/surf/summary_test.go, modules/surf/accesslog_test.go, modules/conga/stats_test.go, modules/lagoon/pending_test.go, modules/lagoon/commands_flags_test.go, modules/cabana/token_json_test.go, modules/flare/commands_test.go, internal/dev/watch_test.go, cmd/summer/runtime_test.go, scripts/check-phase19.8.sh + + - TestServeSummaryLines (verbosity 0 block, verbosity 1 per-plugin lines, quiet prints no block, DSN user and password never present, keyword-form DSN and unparsable DSN), TestServeRecords (one "startup summary" record with every fact, one record whose message equals "listening on ", HideOnTTY used only when the block printed), TestShutdownReportAborted (in-flight requests past the deadline counted as aborted, drain error makes Run return it), TestServeGracefulNil. + - TestAccessLogRequestIDCap (300-byte X-Request-Id stored as 128 bytes, control characters trimmed), TestAccessLogDefaultOn, TestAccessLogDisabled (http.access_log false), TestAccessLogNeverLogsPath (a token-bearing path and query never appear for matched, 404 and 405 routes), TestAccessLogComponentHTTP. + - TestWorkerStats (done, failed and in-flight counters through runAttempt with fake job functions). + - TestPendingMigrations (Docker: none applied, some applied, missing history table) and TestMigratePretend (Docker: lists pending IDs, applies nothing), TestRollbackPretend, TestDeferredPurgePretend (count only, no rows deleted). + - TestConfirmProductionNonTTYProceeds (production, non-interactive console: migrate, migrate:rollback, queue:clear, deferred:purge run without reading stdin), TestConfirmProductionInteractiveNo (interactive console answering no returns a Reported cancel), TestForceSkipsConfirm. + - TestKeyGenerateJSON and TestKeyGenerateQuiet (bare key under -q), TestMigrateStatusJSON, TestRouteListJSON, TestTokenCreateJSON and TestTokenListJSON (cabana, Docker or a fake store as the package's tests allow), TestTokenCreateQuietKeepsToken. + - TestFlareReported (generate and test-push failures return errors matching bonfire.ErrReported and print one message). + - TestStartBinServe (dev passes "serve" then the forwarded globals), TestDevReport (rebuild, failure and restart counts), TestDevGracefulNil. + - TestDelegateExamples (every delegate has examples rendered with summer, and Long mentions that global flags pass through). + + modules/surf/summary.go, modules/surf/serve.go, modules/surf/accesslog.go, modules/surf/accesslog_test.go, modules/surf/serve_summary_smoke_test.go, modules/surf/serve_lifecycle_test.go, modules/conga/worker.go, modules/conga/commands.go, modules/lagoon/migrations.go, modules/lagoon/commands.go, modules/lagoon/keygen.go, modules/lagoon/postgres_test.go, modules/cabana/token_commands.go, modules/cabana/commands_test.go, modules/flare/commands.go, internal/dev/watch.go, internal/dev/watch_test.go, cmd/summer/runtime.go, cmd/summer/runtime_test.go, scripts/check-phase19.8.sh, scripts/check-phase19.4.sh (additive check and NOT RUN rules), 19.8-03-SUMMARY.md (tag, swept plugins, bumped applications, landing and deck commits) + Write the tests in the behavior list (Docker ones skip under -short through the packages' TestMain that calls flag.Parse and testing.Short). Fix exposed bugs in separate `fix(): ...` commits and list released ones in the SUMMARY's Post-release fixes table. + +Then implement the gate's remaining stages in scripts/check-phase19.8.sh. `hello`: `go -C examples/hello vet ./...`, `go -C examples/hello test -count=1 ./...` and required names TestBuiltBinaryListsCommands, TestGreeterDocumented, TestHelpListsServe, TestBuiltBinaryUnknownCommandExitsNonZero. `docs`: the decision note names all six modules; TestDocsTree; `go run ./cmd/summer docs:build --check`; no file under modules/diary, modules/bonfire, docs/console or docs/services/logging.md names a consuming application (names built from split strings); every listed command in `go run ./cmd/summer list --format=json` has examples. `plugins`: for each swept repository from 19.8-03-SUMMARY.md (default list: the 12 D-20 repositories, sm-golem-plugin and sm-feedback-plugin under FONOTEKA, sm-user-plugin under FONOTEKA unless plan 3 deferred it, printed as NOT SWEPT): `go vet ./...`, `go test -count=1 -short ./...`, its `TestCommandsDocumented` passed by name (warn-only, D-17), `GOWORK=off go build ./...`, and the additive check: find the sweep commit by its subject, and in `git diff ^ -- '*.go' ':!*_test.go'` no removed line declares an exported func, method or type and no removed `Name: ""` literal in console files is missing from the added lines. `apps`: for each application, `go build ./...`, the generated main.go has no diff after `summer build` in a temporary copy (or `git diff --exit-code main.go` after build), `bin/ list` and `bin/ help migrate` exit 0. `landing`: `npm --prefix $SUMMERCMSIO_APP/vue-summercmsio-app run test`, the Go terminal tests of sm-summercmsio-app, sm-demo-app's roadmap tests and `scripts/sync-roadmap.sh --check`. `docker`: TestServeLifecycle, TestPendingMigrations, TestMigratePretend and the cabana JSON tests by name without -short; NOT RUN rules as in the 19.4 gate. Add every new name from this task to the go and docker lists, extend --self-test with one planted case per new detector (additive-check removal, NOT SWEPT handling). + +Run `bash scripts/check-phase19.8.sh --all` on a Docker host until green and paste its stage summary into the SUMMARY. Post a short comment on the Phase 19.8 milestone task in g15office (ID recorded in .planning/G15OFFICE.md by plan 03) that the phase is executed and waiting on verification; if unavailable, record that. Commits: `test(surf,conga,lagoon): summaries, reports, stats and checklist flags`, `test(cabana,flare,dev,summer): json, reported errors and dev serve`, `test(19.8): gate stages for hello, docs, plugins, apps, landing and docker`. + + go vet ./... && go test -count=1 -short ./... && bash scripts/check-phase19.8.sh --self-test && LOG="${TMPDIR:-/tmp}/check-phase19.8.log" && bash scripts/check-phase19.8.sh --all >"$LOG" 2>&1 && tail -20 "$LOG" && ! grep -qE '^(FAIL|stage [a-z]+: not implemented)' "$LOG" + Non-zero exit from vet, the short suite, the self-test or --all (any stage red, docker red with PHASE198_REQUIRE_DOCKER=1), or the log still contains a FAIL line or a "not implemented" stage. + + + - `grep -c 'not implemented' scripts/check-phase19.8.sh` prints 0. + - `grep -cE 'TestAccessLogNeverLogsPath|TestConfirmProductionNonTTYProceeds|TestStartBinServe|TestWorkerStats|TestPendingMigrations' scripts/check-phase19.8.sh` prints at least 5. + - `grep -c 'GOWORK=off go' scripts/check-phase19.8.sh` prints at least 1 and `grep -c 'NOT RUN' scripts/check-phase19.8.sh` prints at least 1. + - The SUMMARY holds the --all stage summary with every stage PASS, or docker as NOT RUN with the reason. + + Every swept framework behaviour has a test, and one green --all run proves the framework, the example application, the docs, every shared plugin, every application, the landing site and the Docker-backed paths together. + + + + Task 5: Go-ahead to release this plan's post-release fixes (presented only when the Post-release fixes table has a row) + 19.8-04-SUMMARY.md draft (Post-release fixes table), 19.8-03-SUMMARY.md (tag, plugin commits, application commits), CLAUDE.md (Commits and concurrency, changelog rule) + + - When the table is empty, the SUMMARY says "no post-release fixes" and this checkpoint and Task 6 are skipped. + - Otherwise the SUMMARY records the reply verbatim under a "Task 5 reply" heading, and before the reply `T=$(git tag --points-at HEAD) && test -z "$T"` exits 0. + + Release the listed fixes now, and with which patch tag? + Tasks 2-4 found bugs in code that is already in the plan 03 tag; each fix is committed separately on master and listed in the Post-release fixes table with the module, the symptom and the test that pins it. Releasing means a patch tag of the framework with a same-day changelog post; plugins only need a require bump when a fix changes an API they use (the table says so per row), and applications only need a regenerate commit when a fix changes the generated main. Nothing is deployed in any option. + + + + + + Reply "release vX.Y.Z", "push-only" or "hold". A release also authorises plugin require bumps and application regenerate commits named in the table, pushed when their unpushed commits are this phase's; nothing is ever force-pushed or deployed. + + + + Task 6: Under the Task 5 reply, publish the post-release fixes: patch tag and changelog post, then the plugin requires and application commits the table names + 19.8-04-SUMMARY.md (draft) records the Task 5 reply, or "no post-release fixes" (then this task records "nothing to publish" and stops). + .planning/phases/19.8-console-commands-overhaul/19.8-04-SUMMARY.md + 19.8-04-SUMMARY.md draft (Post-release fixes table, Task 5 reply), 19.8-03-PLAN.md Task 7 (release steps to repeat), ../sm-summercmsio-app/DEPLOY.md ("Posting to the changelog") + On "release vX.Y.Z": same push rules and steps as plan 03 Task 7 step (1) (fetch, origin..master holds only approved commits, vet and short tests green, annotated tag with the fixes listed, push master then the tag, changelog post with user-facing notes under ## Fixed, never print the token); then for each plugin row marked "needs require": `go get git.golem15.com/golem15/summercms@vX.Y.Z`, `go mod tidy`, vet and short tests, one commit `chore: require SummerCMS vX.Y.Z`, push; then for each application row marked "regenerate": regenerate main.go, build, `bin/ list`, one commit `build: regenerate main for SummerCMS vX.Y.Z`, push. On "push-only": push framework master only. On "hold": nothing. Record every tag, push range and the changelog post URL in the SUMMARY, then re-run `bash scripts/check-phase19.8.sh --all` and paste the summary. + + LOG="${TMPDIR:-/tmp}/check-phase19.8-post.log" && bash scripts/check-phase19.8.sh --all >"$LOG" 2>&1 && tail -20 "$LOG" && ! grep -q '^FAIL' "$LOG" + Non-zero exit from the gate, or a line starting with FAIL in its log (for example a plugin no longer builds at the patch tag). On "hold" or with no fixes the command is not run and the SUMMARY says nothing changed. + + + - On release: `git ls-remote --exit-code --tags origin vX.Y.Z` exits 0 and the SUMMARY holds the changelog post URL. + - The SUMMARY lists each published repository with its pushed range, or "nothing to publish". + + Any fix to released code reaches its consumers only with the user's go-ahead, and the gate is green afterwards. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| test fixtures and goldens to the repository | committed files are public with the next push | +| the gate to sibling repositories | the gate builds applications and reads plugin history without committing there | +| this machine to git.golem15.com and summercms.io | only post-release fixes, only after the Task 5 reply | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.8-21 | Repudiation | scripts/check-phase19.8.sh | medium | mitigate | fails on failed, skipped, zero or missing required tests and on "no tests to run"; --self-test proves each detector on planted output; unimplemented stages keep --all red | +| T-19.8-22 | Information disclosure | goldens and fixtures | low | mitigate | goldens use fixed fake values; acceptance scans testdata for JWT, sk- keys and private key headers | +| T-19.8-23 | Tampering | released code with bugs | medium | mitigate | fixes in separate commits, listed with their pinning test; published only under the Task 5 reply with a patch tag and changelog | +| T-19.8-24 | Tampering | shared plugin contracts | medium | mitigate | the plugins stage's additive check fails on any removed exported declaration or command or flag name in a sweep commit | +| T-19.8-25 | Information disclosure | request log regression | high | mitigate | TestAccessLogNeverLogsPath covers matched, 404 and 405 routes with token-bearing paths and queries, run in the go stage | +| T-19.8-SC | Tampering | dependencies | low | accept | this plan adds no module; tests use the standard library, testing/slogtest and the already-required testcontainers | + + + +- Task 1: --self-test and the go stage. +- Task 2: diary, sunscreen, conga and lagoon unit tests with -race; slogtest. +- Task 3: bonfire goldens and unit tests with -race; docsite command word. +- Task 4: framework command tests; `--all` green across every stage (docker NOT RUN only without a daemon). +- Tasks 5-6: only with post-release fixes; reply recorded; gate re-run. +- Feedback sampling: after each test commit `go test -short` of the touched packages plus `bash scripts/check-phase19.8.sh go`; before /msd-verify-work, `bash scripts/check-phase19.8.sh --all`. + + + +- The phase gate exists, is fail-closed and green with --all. +- Unit and golden tests cover diary, bonfire and every swept framework behaviour (project rule: unit tests in the last plan). +- Shared plugins' warn-only coverage tests and additive checks run from the gate; applications build and boot list. +- Released code receives fixes only through the recorded go-ahead. + + + +Create `.planning/phases/19.8-console-commands-overhaul/19.8-04-SUMMARY.md` when done, with the test commits, any fix commits and the Post-release fixes table (module, symptom, pinning test, in tag yes or no, needs require, regenerate), the --all stage summary, Docker tests run or NOT RUN, the Task 5 reply (or "no post-release fixes"), anything published in Task 6, and the g15office comment. + diff --git a/.planning/phases/19.8-console-commands-overhaul/19.8-RESEARCH.md b/.planning/phases/19.8-console-commands-overhaul/19.8-RESEARCH.md index 33221ae4..46f76c02 100644 --- a/.planning/phases/19.8-console-commands-overhaul/19.8-RESEARCH.md +++ b/.planning/phases/19.8-console-commands-overhaul/19.8-RESEARCH.md @@ -663,16 +663,16 @@ Not in D-20 but carrying commands: `sm-golem-plugin`, `sm-feedback-plugin` (`Imp | A6 | Logfmt (`TextHandler`) as the `format: auto` non-TTY default | Pattern 2 | D-05 allows JSON or logfmt; logfmt keeps today's text and smoke greps | | A7 | Daily file dates use the process local time zone (not `app.timezone`) | Pattern 3 | File boundaries differ from the scheduler's | -## Open Questions +## Open Questions (RESOLVED) -1. **Request log path and default (D-11 vs the security contract).** What we know: paths are never logged by design, and the log is off by default via `http.access_log`. Recommendation: log `pattern` + `request_id` (no path), and ask whether to flip `http.access_log` to default-on with `components.http` as the silencer. -2. **`-q` semantics for data output.** Recommendation: keep data/`--json`/errors (Pitfall 10). -3. **Production confirmation on `migrate`/destructive commands.** Recommendation: TTY-only confirmation, non-TTY unchanged, `--force` skips (Pitfall 11). -4. **`summer dev` should run `serve`.** Recommendation: yes, with forwarded verbosity (Pitfall 14). -5. **Exit code after a signal-initiated graceful shutdown** (0 or 130). Recommendation: 0 when the shutdown report has no drain errors. -6. **`sm-golem-plugin` / `sm-feedback-plugin` commands** are outside the D-20 list. Include them in the sweep or leave them. -7. **Framework release tag for plugin `require` bumps.** Plugin commits using new APIs compile via replace, but tagged consumers need a framework tag; the user's go-ahead is needed for tag and push. -8. **Selector widget scope.** No command needs multi-select today. Recommendation: huh Select for `Choice`; port SSU's SelectorModel only when a command needs it (keeps plan 2 lean). +1. **Request log path and default (D-11 vs the security contract).** What we know: paths are never logged by design, and the log is off by default via `http.access_log`. Recommendation: log `pattern` + `request_id` (no path), and ask whether to flip `http.access_log` to default-on with `components.http` as the silencer. RESOLVED: D-23. +2. **`-q` semantics for data output.** Recommendation: keep data/`--json`/errors (Pitfall 10). RESOLVED: D-27. +3. **Production confirmation on `migrate`/destructive commands.** Recommendation: TTY-only confirmation, non-TTY unchanged, `--force` skips (Pitfall 11). RESOLVED: D-28. +4. **`summer dev` should run `serve`.** Recommendation: yes, with forwarded verbosity (Pitfall 14). RESOLVED: D-25. +5. **Exit code after a signal-initiated graceful shutdown** (0 or 130). Recommendation: 0 when the shutdown report has no drain errors. RESOLVED: D-29. +6. **`sm-golem-plugin` / `sm-feedback-plugin` commands** are outside the D-20 list. Include them in the sweep or leave them. RESOLVED: D-24. +7. **Framework release tag for plugin `require` bumps.** Plugin commits using new APIs compile via replace, but tagged consumers need a framework tag; the user's go-ahead is needed for tag and push. RESOLVED: D-26. +8. **Selector widget scope.** No command needs multi-select today. Recommendation: huh Select for `Choice`; port SSU's SelectorModel only when a command needs it (keeps plan 2 lean). RESOLVED: D-30. ## Environment Availability diff --git a/.planning/phases/19.8-console-commands-overhaul/COVERAGE.md b/.planning/phases/19.8-console-commands-overhaul/COVERAGE.md new file mode 100644 index 00000000..3a2ce940 --- /dev/null +++ b/.planning/phases/19.8-console-commands-overhaul/COVERAGE.md @@ -0,0 +1,3 @@ +# Phase 19.8 API coverage + +No external API integration: phase restyles local console output and logging using in-process Go libraries.