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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/code-converge
tmp/
.symphony-workspace/
.start-issue/runs/
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,21 @@

## Root help

`code-converge -h` and `code-converge --help` are equivalent, write the following usage line to stdout, and exit `0` without loading configuration or starting an update or review workflow:
`code-converge -h` and `code-converge --help` are equivalent, write a concise command and global-option reference to stdout, and exit `0` without loading configuration or starting an update or review workflow. Root help lists the `config` and `update` commands, all supported global options, and points to this README for the complete configuration reference. Use `code-converge config --help` for the configuration command syntax, and `code-converge update --help` for update syntax including `--yes` / `-y`.

```text
usage: code-converge [flags] [config]

Commands:
config Show effective configuration and its sources.
update [--yes|-y] Check for and install a newer release.

Global options:
Output:
--log-format Workflow output format: human or kv.
...

See README.md for the full configuration reference.
```

## Workflow
Expand Down
98 changes: 75 additions & 23 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,37 @@ import (

type optionalFlag struct{ target *config.OptionalString }

type globalFlagSpec struct {
name, group, description string
bind func(*flag.FlagSet, *config.Overrides)
}

var globalFlagSpecs = []globalFlagSpec{
{"log-format", "Output", "Workflow output format: human or kv.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "log-format", &o.LogFormat) }},
{"heartbeat", "Output", "Human-output liveness interval.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "heartbeat", &o.Heartbeat) }},
{"color", "Output", "Interactive human-output color: auto, always, or never.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "color", &o.Color) }},
{"mode", "Workflow", "Execution profile: fast or best.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "mode", &o.Mode) }},
{"max-cycles", "Workflow", "Maximum fix-findings attempts per review phase.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "max-cycles", &o.MaxCycles) }},
{"max-ci-recoveries", "Workflow", "Maximum CI recovery attempts.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "max-ci-recoveries", &o.MaxCIRecoveries) }},
{"review-model", "Stage overrides", "Review model.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "review-model", &o.ReviewModel) }},
{"review-reasoning-effort", "Stage overrides", "Review reasoning effort.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "review-reasoning-effort", &o.ReviewEffort) }},
{"fix-model", "Stage overrides", "Fix-findings model.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "fix-model", &o.FixModel) }},
{"fix-reasoning-effort", "Stage overrides", "Fix-findings reasoning effort.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "fix-reasoning-effort", &o.FixEffort) }},
{"fix-prompt-file", "Stage overrides", "Fix-findings prompt file.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "fix-prompt-file", &o.FixPromptPath) }},
{"finalize-model", "Stage overrides", "Finalization model.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "finalize-model", &o.FinalizeModel) }},
{"finalize-reasoning-effort", "Stage overrides", "Finalization reasoning effort.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "finalize-reasoning-effort", &o.FinalizeEffort) }},
{"finalize-prompt-file", "Stage overrides", "Finalization prompt file.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "finalize-prompt-file", &o.FinalizePromptPath) }},
{"ci-fix-model", "Stage overrides", "CI-fix model.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "ci-fix-model", &o.CIFixModel) }},
{"ci-fix-reasoning-effort", "Stage overrides", "CI-fix reasoning effort.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "ci-fix-reasoning-effort", &o.CIFixEffort) }},
{"ci-fix-prompt-file", "Stage overrides", "CI-fix prompt file.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "ci-fix-prompt-file", &o.CIFixPromptPath) }},
{"review-base", "Workflow", "Review base override.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "review-base", &o.ReviewBase) }},
{"session-log-dir", "Diagnostics", "Diagnostic session-log directory.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "session-log-dir", &o.SessionLogDir) }},
{"session-log-retention", "Diagnostics", "Diagnostic session-log retention.", func(f *flag.FlagSet, o *config.Overrides) { bind(f, "session-log-retention", &o.SessionLogRetention) }},
{"no-session-log", "Diagnostics", "Disable diagnostic session logging.", func(f *flag.FlagSet, o *config.Overrides) {
f.BoolVar(&o.NoSessionLog, "no-session-log", false, "disable diagnostic session logging")
}},
}

func (f optionalFlag) String() string {
if f.target == nil {
return ""
Expand Down Expand Up @@ -58,8 +89,7 @@ func (a App) Run(ctx context.Context, args []string) int {
if stderr == nil {
stderr = os.Stderr
}
if len(args) == 1 && (args[0] == "-h" || args[0] == "--help") {
rootUsage(stdout)
if helpCommand(stdout, args) {
return workflow.ExitSuccess
}
if len(args) == 1 && args[0] == "--version" {
Expand Down Expand Up @@ -92,27 +122,9 @@ func (a App) Run(ctx context.Context, args []string) int {
configCommand := len(args) > 0 && args[0] == "config"
flags := flag.NewFlagSet("code-converge", flag.ContinueOnError)
flags.SetOutput(io.Discard)
bind(flags, "log-format", &overrides.LogFormat)
bind(flags, "heartbeat", &overrides.Heartbeat)
bind(flags, "color", &overrides.Color)
bind(flags, "mode", &overrides.Mode)
bind(flags, "max-cycles", &overrides.MaxCycles)
bind(flags, "max-ci-recoveries", &overrides.MaxCIRecoveries)
bind(flags, "review-model", &overrides.ReviewModel)
bind(flags, "review-reasoning-effort", &overrides.ReviewEffort)
bind(flags, "fix-model", &overrides.FixModel)
bind(flags, "fix-reasoning-effort", &overrides.FixEffort)
bind(flags, "fix-prompt-file", &overrides.FixPromptPath)
bind(flags, "finalize-model", &overrides.FinalizeModel)
bind(flags, "finalize-reasoning-effort", &overrides.FinalizeEffort)
bind(flags, "finalize-prompt-file", &overrides.FinalizePromptPath)
bind(flags, "ci-fix-model", &overrides.CIFixModel)
bind(flags, "ci-fix-reasoning-effort", &overrides.CIFixEffort)
bind(flags, "ci-fix-prompt-file", &overrides.CIFixPromptPath)
bind(flags, "review-base", &overrides.ReviewBase)
bind(flags, "session-log-dir", &overrides.SessionLogDir)
bind(flags, "session-log-retention", &overrides.SessionLogRetention)
flags.BoolVar(&overrides.NoSessionLog, "no-session-log", false, "disable diagnostic session logging")
for _, spec := range globalFlagSpecs {
spec.bind(flags, &overrides)
}

if len(args) > 0 && args[0] == "config" {
args = append(append([]string{}, args[1:]...), "config")
Expand Down Expand Up @@ -223,6 +235,46 @@ func (a App) Run(ctx context.Context, args []string) int {

func rootUsage(out io.Writer) {
fmt.Fprintln(out, "usage: code-converge [flags] [config]")
fmt.Fprintln(out, "")
fmt.Fprintln(out, "Commands:")
fmt.Fprintln(out, " config Show effective configuration and its sources.")
fmt.Fprintln(out, " update [--yes|-y] Check for and install a newer release.")
fmt.Fprintln(out, "")
fmt.Fprintln(out, "Global options:")
group := ""
for _, spec := range globalFlagSpecs {
if spec.group != group {
group = spec.group
fmt.Fprintf(out, " %s:\n", group)
}
fmt.Fprintf(out, " --%-25s %s\n", spec.name, spec.description)
}
fmt.Fprintln(out, "")
fmt.Fprintln(out, "See README.md for the full configuration reference.")
}

func helpCommand(out io.Writer, args []string) bool {
if len(args) == 1 && (args[0] == "-h" || args[0] == "--help") {
rootUsage(out)
return true
}
if len(args) != 2 || (args[1] != "-h" && args[1] != "--help") {
return false
}
switch args[0] {
case "config":
fmt.Fprintln(out, "usage: code-converge config [global options]")
fmt.Fprintln(out, "")
fmt.Fprintln(out, "Show effective configuration values and their sources without starting a workflow.")
return true
case "update":
fmt.Fprintln(out, "usage: code-converge update [--yes|-y]")
fmt.Fprintln(out, "")
fmt.Fprintln(out, "Check for and install a newer release. --yes and -y skip confirmation.")
return true
default:
return false
}
}

func updateArgs(args []string) (bool, error) {
Expand Down
43 changes: 42 additions & 1 deletion internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,20 @@ func TestRootHelpAliasesExitBeforeOperationalSetup(t *testing.T) {
Runner: fake,
Updater: updater,
}).Run(context.Background(), args)
if code != workflow.ExitSuccess || stdout.String() != "usage: code-converge [flags] [config]\n" || stderr.Len() != 0 {
for _, want := range []string{
"usage: code-converge [flags] [config]",
"config Show effective configuration",
"update [--yes|-y]",
"Global options:",
"--log-format",
"--no-session-log",
"See README.md for the full configuration reference.",
} {
if !strings.Contains(stdout.String(), want) {
t.Errorf("missing %q in help:\n%s", want, stdout.String())
}
}
if code != workflow.ExitSuccess || stderr.Len() != 0 {
t.Fatalf("code=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
}
if len(fake.invocations) != 0 || updater.called {
Expand All @@ -137,6 +150,34 @@ func TestRootHelpAliasesExitBeforeOperationalSetup(t *testing.T) {
}
}

func TestSubcommandHelpExitsBeforeOperationalSetup(t *testing.T) {
tests := []struct {
args []string
wants []string
}{
{[]string{"config", "--help"}, []string{"usage: code-converge config [global options]", "Show effective configuration"}},
{[]string{"config", "-h"}, []string{"usage: code-converge config [global options]"}},
{[]string{"update", "--help"}, []string{"usage: code-converge update [--yes|-y]", "--yes and -y skip confirmation"}},
{[]string{"update", "-h"}, []string{"usage: code-converge update [--yes|-y]"}},
}
for _, test := range tests {
t.Run(strings.Join(test.args, " "), func(t *testing.T) {
var stdout, stderr bytes.Buffer
fake := &appFakeRunner{t: t}
updater := &appFakeUpdater{code: workflow.ExitOperational}
code := (App{Stdout: &stdout, Stderr: &stderr, Runner: fake, Updater: updater}).Run(context.Background(), test.args)
if code != workflow.ExitSuccess || stderr.Len() != 0 || len(fake.invocations) != 0 || updater.called {
t.Fatalf("code=%d stdout=%q stderr=%q invocations=%#v updater.called=%v", code, stdout.String(), stderr.String(), fake.invocations, updater.called)
}
for _, want := range test.wants {
if !strings.Contains(stdout.String(), want) {
t.Errorf("missing %q in help:\n%s", want, stdout.String())
}
}
})
}
}

func TestUpdateCommandDispatchesWithoutStartingWorkflow(t *testing.T) {
var stdout, stderr bytes.Buffer
updater := &appFakeUpdater{code: 0}
Expand Down
19 changes: 19 additions & 0 deletions memory-bank/features/FT-036/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: "FT-036: Discoverable CLI Help"
doc_kind: feature
doc_function: index
purpose: "Навигация по canonical problem, solution и execution artifacts для discoverable CLI help из issue #36."
derived_from:
- ../../dna/governance.md
- brief.md
status: active
audience: humans_and_agents
---

# FT-036: Discoverable CLI Help

## Аннотированный индекс

- [`brief.md`](brief.md) — canonical problem space, scope, validation profile и verify contract.
- [`design.md`](design.md) — selected CLI help contract и invariants.
- [`implementation-plan.md`](implementation-plan.md) — grounding и execution sequence.
117 changes: 117 additions & 0 deletions memory-bank/features/FT-036/brief.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
---
title: "FT-036: Discoverable CLI Help"
doc_kind: feature
doc_function: canonical
purpose: "Canonical problem, scope, validation profile и verify contract для discoverable CLI help из issue #36."
derived_from:
- ../../flows/feature.md
- ../../engineering/validation-profiles.md
- ../../../README.md
status: active
delivery_status: done
audience: humans_and_agents
must_not_define:
- implementation_sequence
- solution_space
---

# FT-036: Discoverable CLI Help

## What

### Problem

Root help currently exposes only a usage line, and `config --help` and `update --help` are treated as invalid invocations. Operators cannot discover commands, flags, configuration entry points, or command-specific syntax without reading the README.

### Outcome

| Metric ID | Metric | Baseline | Target | Measurement method |
| --- | --- | --- | --- | --- |
| `MET-01` | Interactive CLI discoverability | Root has one usage line; subcommand help exits 2 | Root and both supported subcommands provide stable successful help | Focused app tests and public README contract |

### Scope

- `REQ-01` `-h` and `--help` render concise root help to stdout and exit 0, including usage, `config` and `update` synopses, global options, and a README/configuration pointer.
- `REQ-02` `config --help` and `update --help` render command-specific stdout help and exit 0; update documents `--yes` and `-y`.
- `REQ-03` Every help invocation returns before configuration loading, diagnostic session logging, workflow start, Codex invocation, and self-update.
- `REQ-04` Focused tests and the public CLI contract describe the help text and exit semantics without changing machine-readable workflow output.

### Non-Scope

- `NS-01` Changing workflow, configuration-command, update-command, or machine-readable event semantics outside help invocations.
- `NS-02` Adding commands, changing flag values/defaults, loading configuration to build help, or performing a self-update check.

### Constraints / Assumptions

- `ASM-01` The existing `flag` definitions are the authoritative inventory of global options.
- `CON-01` Public CLI output must remain concise and stable; README remains the complete configuration reference.

## Design Requirement Decision

| Decision | Reason | Downstream owner |
| --- | --- | --- |
| `Design required: yes` | The public CLI/help and stdout contract changes; command dispatch must preserve non-help side-effect boundaries. | `design.md` |

## Artifact Routing Decision

| Artifact | Decision | Trigger / reason | Route / owner |
| --- | --- | --- | --- |
| Separate use-case or runtime-surface artifact | omitted | One CLI entrypoint and three help paths are fully traceable in the brief and design. | `none` |

## Validation Profile Decision

| Profile | Triggers / rationale | Downgrade approval |
| --- | --- | --- |
| `standard` | Executable public CLI contract and exit semantics change. No security, data, integration, release, or rollout trigger applies. | `none` |

## Verify

### Exit Criteria

- `EC-01` All three help surfaces satisfy their documented output and exit contract without side effects.
- `EC-02` Normal workflow and machine-readable output remain unaffected.

### Traceability matrix

| Requirement ID | Problem refs | Acceptance refs | Checks | Evidence IDs |
| --- | --- | --- | --- | --- |
| `REQ-01` | `ASM-01`, `CON-01` | `EC-01`, `SC-01` | `CHK-01`, `CHK-03` | `EVID-01`, `EVID-03` |
| `REQ-02` | `CON-01` | `EC-01`, `SC-02` | `CHK-01`, `CHK-03` | `EVID-01`, `EVID-03` |
| `REQ-03` | `CON-01` | `EC-01`, `SC-03` | `CHK-02` | `EVID-02` |
| `REQ-04` | `CON-01` | `EC-02`, `SC-04` | `CHK-03`, `CHK-04` | `EVID-03`, `EVID-04` |

### Acceptance Scenarios

- `SC-01` An operator runs `code-converge --help` or `-h` and receives root usage, command synopses, grouped global options, and a README/configuration pointer with exit 0.
- `SC-02` An operator runs `code-converge config --help` or `code-converge update --help` and receives the valid syntax and purpose; update lists both confirmation aliases, with exit 0.
- `SC-03` A help invocation with fake runner and updater performs neither a runner invocation nor self-update.
- `SC-04` A normal invalid/workflow invocation retains its pre-existing event and exit behavior.

### Negative Coverage

- `NEG-01` Invalid `update` arguments still exit operationally and do not become a successful help path.

### Checks

| Check ID | Covers | How to check | Expected result | Evidence path |
| --- | --- | --- | --- | --- |
| `CHK-01` | `SC-01`, `SC-02`, `NEG-01` | Focused `internal/app` tests | Exact required help fragments and exit codes pass | `go test ./internal/app` |
| `CHK-02` | `SC-03` | Fake-runner/updater tests | No configuration/workflow/update side effect on help | `go test ./internal/app` |
| `CHK-03` | `SC-01`, `SC-02`, `SC-04` | `go test ./...`, `go vet ./...`, `git diff --check` | All suites pass and diff is valid | command output |
| `CHK-04` | `REQ-04` | `make docs-lint` | Public docs links and governed frontmatter pass | command output |

### Test matrix

| Check ID | Evidence IDs | Evidence path |
| --- | --- | --- |
| `CHK-01` | `EVID-01` | `go test ./internal/app` |
| `CHK-02` | `EVID-02` | `go test ./internal/app` |
| `CHK-03` | `EVID-03` | local command output and CI run |
| `CHK-04` | `EVID-04` | `make docs-lint` |

### Evidence

- `EVID-01` Focused app-test output covering root and subcommand help.
- `EVID-02` Test assertions showing help bypasses runner/updater.
- `EVID-03` Full local suite, vet, diff-check, and required CI result.
- `EVID-04` Documentation-lint output and README contract review.
Loading