Skip to content

Commit 8ded158

Browse files
committed
Add AGENTS.md for codding agents
1 parent 6918172 commit 8ded158

4 files changed

Lines changed: 139 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# lets
2+
3+
@README.md
4+
5+
## Tools
6+
7+
Use `lets` task runner for all build/test/lint operations instead of raw commands. Run `lets build` first if binary is missing.
8+
9+
```bash
10+
lets build [bin] # build CLI with version metadata
11+
lets build-and-install # build and install lets-dev locally
12+
lets test # full suite: unit + bats + completions
13+
lets test-unit # Go unit tests only
14+
lets test-bats [test] # Docker-based Bats integration tests
15+
lets lint # golangci-lint via Docker
16+
lets fmt # go fmt ./...
17+
lets coverage [--html] # coverage report
18+
lets run-docs # local docs dev server (docs/)
19+
lets publish-docs # deploy docs site
20+
```
21+
22+
`lets test-unit`, `lets test-bats`, and `lets lint` require Docker. Use `go test ./...` locally for quick iteration without Docker.
23+
24+
## Agent Behavior
25+
26+
- **Proactive execution** — Don't ask "Can I proceed?" for implementation. DO ask before changing success criteria, test thresholds, or what "working" means.
27+
- **Test early, test real** — Don't accumulate 10 changes then debug. After each logical step: does it work? With realistic input, not just edge case that triggered the work.
28+
- **Pushback** — Propose alternatives before implementing suboptimal approaches. Ask about design choices.
29+
- **Unify, don't duplicate** — Merge nearly-identical structs/functions rather than adding variants.
30+
- **No over-engineering** — Minimum complexity for current task. No speculative abstractions.
31+
- **Terseness** — Comments for surprising/hairy logic only. Be extremely concise in communication.
32+
33+
## Package Structure
34+
35+
- `main.go` — entry point, flag parsing, signal handling
36+
- `cmd/` — Cobra commands (root, subcommands, completion, LSP, self-update)
37+
- `config/` — config file discovery, loading, validation; `config/config/` defines Config/Command/Mixin structs and YAML unmarshaling
38+
- `executor/` — command execution, dependency resolution, env setup, checksum verification
39+
- `env/` — debug level state (`LETS_DEBUG`, levels 0-2)
40+
- `logging/` — logrus-based logging with command chain formatting
41+
- `lsp/` — Language Server Protocol: definition lookup, completion for depends, tree-sitter YAML parsing; `lets lsp` runs stdio-based server for IDE integration
42+
- `checksum/` — SHA1 file checksumming with glob patterns
43+
- `docopt/` — docopt argument parsing, produces `LETSOPT_*` and `LETSCLI_*` env vars
44+
- `upgrade/` — binary self-update from GitHub releases
45+
- `util/` — file/dir/version helpers
46+
- `workdir/``--init` scaffolding
47+
- `set/` — generic Set data structure
48+
- `test/` — test utilities (temp files, args helpers)
49+
50+
## Key lets.yaml Fields
51+
52+
- Top-level: `shell`, `env`, `eval_env`, `before`, `init`, `mixins`, `commands`
53+
- Command: `cmd`, `description`, `depends`, `env`, `options` (docopt), `work_dir`, `after`, `checksum`, `persist_checksum`, `ref`, `args`, `shell`
54+
55+
## Project Rules
56+
57+
- Follow `gofmt` exactly; tabs for indentation, ~120 char lines
58+
- Unit tests as `*_test.go` next to source; Bats tests in `tests/*.bats`
59+
- Fixtures in matching `tests/<scenario>/` folder, use `lets.yaml` unless variant needed
60+
- Bats tests use `run` + `assert_success`/`assert_line` pattern
61+
- Run at least `go test ./...` before considering work complete; `lets test-bats` for CLI-path changes
62+
- Commits: short imperative subjects (`Add ...`, `Fix ...`, `Use ...`), explain non-obvious context in body
63+
- **Changelog workflow**: add entries to the `Unreleased` section in `docs/docs/changelog.md` with each commit/PR. At release time, rename `Unreleased` to the new tag version
64+
- Do not commit `lets.my.yaml`, generated binaries, `.lets/`, `coverage.out`, or `node_modules`
65+
- CLI flags: kebab-case only (`--dry-run` not `--dry_run`)
66+
- No "Generated by <agent>" in commits

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

cmd/root_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,32 @@ func TestRootCmdWithConfig(t *testing.T) {
110110
t.Fatalf("expected foo suggestion, got %q", err.Error())
111111
}
112112
})
113+
114+
t.Run("should return exit code 2 for unknown command with no suggestions", func(t *testing.T) {
115+
rootCmd, _ := newTestRootCmdWithConfig([]string{"zzzznotacommand"})
116+
117+
err := rootCmd.Execute()
118+
if err == nil {
119+
t.Fatal("expected unknown command error")
120+
}
121+
122+
var exitCoder interface{ ExitCode() int }
123+
if !errors.As(err, &exitCoder) {
124+
t.Fatal("expected error with exit code")
125+
}
126+
127+
if exitCode := exitCoder.ExitCode(); exitCode != 2 {
128+
t.Fatalf("expected exit code 2, got %d", exitCode)
129+
}
130+
131+
if !strings.Contains(err.Error(), `unknown command "zzzznotacommand"`) {
132+
t.Fatalf("expected unknown command error, got %q", err.Error())
133+
}
134+
135+
if strings.Contains(err.Error(), "Did you mean this?") {
136+
t.Fatalf("expected no suggestions, got %q", err.Error())
137+
}
138+
})
113139
}
114140

115141
func TestSelfCmd(t *testing.T) {
@@ -148,4 +174,36 @@ func TestSelfCmd(t *testing.T) {
148174
t.Fatalf("expected lsp suggestion, got %q", err.Error())
149175
}
150176
})
177+
178+
t.Run("should return exit code 2 for unknown self subcommand with no suggestions", func(t *testing.T) {
179+
bufOut := new(bytes.Buffer)
180+
181+
rootCmd := CreateRootCommand("v0.0.0-test")
182+
rootCmd.SetArgs([]string{"self", "zzzznotacommand"})
183+
rootCmd.SetOut(bufOut)
184+
rootCmd.SetErr(bufOut)
185+
InitSelfCmd(rootCmd, "v0.0.0-test")
186+
187+
err := rootCmd.Execute()
188+
if err == nil {
189+
t.Fatal("expected unknown command error")
190+
}
191+
192+
var exitCoder interface{ ExitCode() int }
193+
if !errors.As(err, &exitCoder) {
194+
t.Fatal("expected error with exit code")
195+
}
196+
197+
if exitCode := exitCoder.ExitCode(); exitCode != 2 {
198+
t.Fatalf("expected exit code 2, got %d", exitCode)
199+
}
200+
201+
if !strings.Contains(err.Error(), `unknown command "zzzznotacommand" for "lets self"`) {
202+
t.Fatalf("expected unknown self subcommand error, got %q", err.Error())
203+
}
204+
205+
if strings.Contains(err.Error(), "Did you mean this?") {
206+
t.Fatalf("expected no suggestions, got %q", err.Error())
207+
}
208+
})
151209
}

tests/command_not_found.bats

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,17 @@ setup() {
2929
assert_output --partial 'Did you mean this?'
3030
assert_output --partial 'lsp'
3131
}
32+
33+
@test "command_not_found: no suggestions for completely unrelated command" {
34+
run lets zzzznotacommand
35+
assert_failure 2
36+
assert_output --partial 'unknown command "zzzznotacommand" for "lets"'
37+
refute_output --partial 'Did you mean this?'
38+
}
39+
40+
@test "command_not_found: no suggestions for completely unrelated self subcommand" {
41+
run lets self zzzznotacommand
42+
assert_failure 2
43+
assert_output --partial 'unknown command "zzzznotacommand" for "lets self"'
44+
refute_output --partial 'Did you mean this?'
45+
}

0 commit comments

Comments
 (0)