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
111 changes: 73 additions & 38 deletions .markdownlint-cli2.yaml
Original file line number Diff line number Diff line change
@@ -1,50 +1,85 @@
# Global markdownlint config for Claude Code hooks
extends: markdownlint/style/prettier

# Global markdownlint-cli2 configuration
# Canonical version: 2026.04.15
# Symlinked to ~/.markdownlint-cli2.yaml via stow
#
# Per-repo copies should preserve the `Canonical version` line above and bump it on resync;
# a stale calver compared to this file is the signal that the per-repo copy has drifted.
#
# Documentation: https://github.com/DavidAnson/markdownlint-cli2
# Rules: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md

# Configure markdownlint rules
config:
# === Not auto-fixable rules (disabled to reduce noise) ===

# Line length - too noisy, not auto-fixable
MD013: false
# Use all default rules
default: true

# Heading increment (h1 -> h3 skip) - varied doc structures
MD001: false
# MD003: Heading style - use ATX style (#)
MD003:
style: "atx"

# Multiple H1s - frontmatter + H1 is common
MD025: false
# MD004: Unordered list style - use dashes
MD004:
style: "dash"

# Blank lines in blockquote - not auto-fixable
MD028: false

# Bare URLs - common and intentional
MD034: false

# Emphasis as heading - used in docs
MD036: false

# First-line heading requirement - PR templates, etc.
MD041: false
# MD007: Unordered list indentation - 2 spaces
MD007:
indent: 2

# Empty links - placeholder links in drafts
MD042: false
# MD009: Trailing spaces - allow 2 spaces for line breaks
MD009:
br_spaces: 2

# Alt-text requirement - not auto-fixable
MD045: false
# MD013: Line length - 120 chars (more reasonable for code docs)
MD013:
line_length: 120
code_blocks: false # Don't check code blocks
tables: false # Don't check tables
headings: false # Don't check headings

# Link fragment validation - complex anchors
MD051: false
# MD024: Allow duplicate headings in different sections
MD024:
siblings_only: true

# Descriptive link text - not auto-fixable
MD059: false
# MD025: Single top-level heading - allow multiple (for changelogs, etc.)
MD025: false

# Table style - not auto-fixable, different styles valid
MD060: false
# MD033: Allow inline HTML for specific elements
MD033:
allowed_elements:
- "br"
- "img"
- "a"
- "details"
- "summary"
- "sub"
- "sup"
- "kbd"

# MD034: Bare URLs - allow (common in docs)
MD034: false

# === Relaxed rules ===
# MD036: Emphasis used as heading - allow (stylistic choice)
MD036: false

# Allow duplicate headings in different sections
MD024:
siblings_only: true
# MD041: First line should be top-level heading - disable (not always needed)
MD041: false

# Allow HTML for advanced formatting
MD033: false
# MD046: Code block style - fenced
MD046:
style: "fenced"

# MD048: Code fence style - backticks
MD048:
style: "backtick"

# Ignore patterns
ignores:
- "node_modules/**"
- "**/node_modules/**"
- "vendor/**"
- "target/**" # Rust build artifacts; harmless for non-Rust projects
- ".git/**"
- "*.min.md"

# Fix automatically when --fix is used
fix: true
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ itself, children spawned without arguments must not recurse into `check .`.

- `0` — all checks passed
- `1` — warnings present, no failures
- `2` — failures or errors detected
- `2` — failures, errors, or usage errors (bare `anc`, unknown flag, mutually exclusive flags, command not found on
PATH)

Exit 2 is overloaded. To distinguish "ran but found problems" from "called
incorrectly", parse stderr — usage errors include `Usage:` text; check failures don't.

## Project Structure

Expand Down
11 changes: 8 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,16 @@ tool as your FIRST action. Do NOT answer directly, do NOT use other tools first.

For the full routing table, see `~/.claude/skills/docs/workflow-routing.md`.

## Documented Solutions

`docs/solutions/` (symlink to `~/dev/solutions-docs/`) — searchable archive of past
solutions and best practices, organized by category with YAML frontmatter (`module`, `tags`, `problem_type`). Search
with `qmd query "<topic>" --collection solutions`. Relevant when implementing or debugging in documented areas.

## gstack Project History

This project was designed in the `brettdavies/agent-skills` repo, then moved here.
gstack project data (design doc, eng review, naming rationale, review history) has been copied to
`~/.gstack/projects/brettdavies-agentnative/`.
This project was designed in the `brettdavies/agent-skills` repo, then moved here. gstack project data (design doc, eng
review, naming rationale, review history) has been copied to `~/.gstack/projects/brettdavies-agentnative/`.

Key decisions already made:

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,11 @@ checks are skipped because there is no source tree to analyze.
|------|---------|
| 0 | All checks passed |
| 1 | Warnings present (no failures) |
| 2 | Failures or errors detected |
| 2 | Failures, errors, or usage errors |

Exit 2 covers both check failures (a real `[FAIL]` or `[ERROR]` result) and usage errors (bare `anc`, unknown flag,
mutually exclusive flags). Agents distinguishing the two should parse `stderr` (usage errors print `Usage:`) or call
`anc --help` first to validate the invocation shape.

### Shell Completions

Expand Down
2 changes: 1 addition & 1 deletion completions/anc.bash
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ _anc() {
fi
case "${prev}" in
--command)
COMPREPLY=($(compgen -f "${cur}"))
COMPREPLY=($(compgen -c "${cur}"))
return 0
;;
--principle)
Expand Down
2 changes: 1 addition & 1 deletion completions/anc.fish
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ complete -c anc -n "__fish_anc_needs_command" -s V -l version -d 'Print version'
complete -c anc -n "__fish_anc_needs_command" -f -a "check" -d 'Check a CLI project or binary for agent-readiness'
complete -c anc -n "__fish_anc_needs_command" -f -a "completions" -d 'Generate shell completions'
complete -c anc -n "__fish_anc_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c anc -n "__fish_anc_using_subcommand check" -l command -d 'Resolve a command from PATH and run behavioral checks against it' -r
complete -c anc -n "__fish_anc_using_subcommand check" -l command -d 'Resolve a command from PATH and run behavioral checks against it' -r -f -a "(__fish_complete_command)"
complete -c anc -n "__fish_anc_using_subcommand check" -l principle -d 'Filter checks by principle number (1-7)' -r
complete -c anc -n "__fish_anc_using_subcommand check" -l output -d 'Output format' -r -f -a "text\t''
json\t''"
Expand Down
2 changes: 1 addition & 1 deletion completions/anc.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ _anc() {
case $line[1] in
(check)
_arguments "${_arguments_options[@]}" : \
'()--command=[Resolve a command from PATH and run behavioral checks against it]:NAME:_default' \
'(--source)--command=[Resolve a command from PATH and run behavioral checks against it]:NAME:_command_names -e' \
'--principle=[Filter checks by principle number (1-7)]:PRINCIPLE:_default' \
'--output=[Output format]:OUTPUT:(text json)' \
'--binary[Run only behavioral checks (skip source analysis)]' \
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
---
title: "feat: Default subcommand (anc .) and --command flag for PATH lookup"
type: feat
status: active
status: shipped
date: 2026-04-02
deepened: 2026-04-02
shipped: 2026-04-15
origin: ~/.gstack/projects/brettdavies-agentnative/brett-main-design-20260327-214808.md
---

# feat: Default subcommand (anc .) and --command flag for PATH lookup

## Status

**Shipped on `dev`** — both implementation units complete plus a follow-on cluster of edge-case fixes surfaced by
post-merge code review.

- PR [#12](https://github.com/brettdavies/agentnative/pull/12) — initial implementation (commit `4afef67`, merged
2026-04-15)
- PR [#13](https://github.com/brettdavies/agentnative/pull/13) — 7 edge-case fixes + refactor surfaced by `/ce-review`
of the merged PR (open against `dev`)
- Pattern documented for reuse: `docs/solutions/best-practices/clap-default-subcommand-via-argv-pre-parse-20260415.md`

See **Post-Implementation Notes** at the end for the delta between the planned design
and what actually shipped.

## Overview

Two CLI contract additions from the design doc: (1) `anc .` should work as shorthand for `anc check .`, making `check`
Expand Down Expand Up @@ -89,17 +104,18 @@ PATH without manually resolving its location — the design doc (line 209) speci

### Deferred to Implementation

- **Typo handling**: `anc chekc .` (typo of `check`) would become `anc check chekc .` where `chekc` becomes the path.
This produces "path does not exist: chekc" instead of "unrecognized subcommand 'chekc'." Acceptable for v0.1 — the
error is still actionable.
- **Clap error message context**: When pre-parse injects `check`, clap error messages reference the `check` subcommand
context. Users who typed `anc . --bogus` see errors mentioning `check` in the usage line. Minor UX imperfection,
acceptable for v0.1.
- **Typo handling** _(status: as planned)_: `anc chekc .` (typo of `check`) becomes `anc check chekc .` where `chekc`
becomes the path. Produces "path does not exist: chekc" instead of "unrecognized subcommand 'chekc'." Acceptable for
v0.1 — the error is still actionable. Note: the `help` subcommand is special-cased in PR #13 because clap's
auto-generated `help` is not returned by `get_subcommands()` and would otherwise hit this path.
- **Clap error message context** _(status: as planned)_: When pre-parse injects `check`, clap error messages reference
the `check` subcommand context. Users who typed `anc . --bogus` see errors mentioning `check` in the usage line. Minor
UX imperfection, acceptable for v0.1.

## High-Level Technical Design

> *This illustrates the intended approach and is directional guidance for review, not implementation specification. The
> implementing agent should treat it as context, not code to reproduce.*
> _This illustrates the intended approach and is directional guidance for review, not implementation specification. The
> implementing agent should treat it as context, not code to reproduce._

```text
argv = ["anc", "-q", ".", "--output", "json"]
Expand Down Expand Up @@ -264,4 +280,76 @@ known subcommand, so pre-parse injects `check`)

- **Design doc:** `~/.gstack/projects/brettdavies-agentnative/brett-main-design-20260327-214808.md` (lines 126, 209)
- **Safety constraint:** `~/dev/solutions-docs/logic-errors/cli-linter-fork-bomb-recursive-self-invocation-20260401.md`
- Related code: `src/cli.rs`, `src/main.rs`, `src/project.rs`, `src/error.rs`
- Related code: `src/cli.rs`, `src/main.rs`, `src/argv.rs`, `src/project.rs`, `src/error.rs`

## Post-Implementation Notes

What the planning sections above don't capture: the design above shipped in PR #12 and worked, but `/ce-review` of the
merged commit surfaced seven edge cases. PR #13 closed all of them. This section is the delta — readers picking up
later need both halves.

### Final code locations

- `src/argv.rs` (new module, ~340 lines including 19 unit tests) — owns `inject_default_subcommand` and the supporting
helpers (`build_known_subcommand_set`, `build_value_flag_set`, `build_top_level_flag_set`, `consumes_next`,
`base_form`). Extracted from `src/main.rs` in PR #13's refactor.
- `src/main.rs` (203 lines, down from 538 mid-PR) — `run()`, `resolve_command_on_path()`, and the `match cli.command`
arm.
- `src/cli.rs` — `Cli` derive plus `after_help` block, `value_hint = ValueHint::CommandName` on `--command`, and
`conflicts_with = "source"` on `--command`.
- `scripts/generate-completions.sh` — `post_process()` function that patches bash completion to swap `compgen -f` →
`compgen -c` for `--command` (clap_complete's bash backend ignores `ValueHint::CommandName`).

### Edge cases resolved beyond the original design

1. **Clap's auto `help` subcommand is NOT in `get_subcommands()`** — `anc help` and `anc help check` were misclassified
as paths. Fixed by appending `"help"` to the known set.
2. **Value-taking flags must be paired with their values during scanning** — `anc --command check` mis-routed because
`check` (the value) was treated as the explicit subcommand. Fixed by walking clap's `get_arguments()` for both `Cli`
and every subcommand to learn which flags consume the next token.
3. **Subcommand-scoped flags imply default-subcommand intent even with no positional** — `anc --command rg` and `anc
--output json --source` produced clap errors. Fixed by tracking whether any encountered flag is subcommand-scoped
(not in the top-level Cli flag set) and injecting `check` if so when no positional was found.
4. **POSIX `--` separator** — `anc -- .` was untested and ill-defined. Now injects `check` before the separator so clap
routes the remaining tokens to Check.
5. **`arg_required_else_help` only fires on zero args** — `anc -q` (or `--quiet`) reaches `match cli.command` with
`command = None` and previously panicked via `unreachable!()`. The `None` arm now renders help to stderr and exits 2.
This was a pre-existing bug, not introduced by this plan, but surfaced under the new ergonomics.
6. **`--command` + `--source` is contradictory** — added `conflicts_with = "source"` so clap rejects the combination at
parse time instead of producing a silent empty result.
7. **Bash completion suggests file paths instead of PATH commands** — added `value_hint = ValueHint::CommandName`.
zsh/fish/elvish honor it; bash needs the post-generation `sed` patch documented above.

### Design-time decisions that survived contact with reality

- `flatten` rejection (Key Technical Decisions §1) — confirmed correct; `flatten` remains incompatible with
`arg_required_else_help`.
- Clap introspection for subcommand list (Key Technical Decisions §2) — proved its worth: introspection-driven flag
catalogues were essential for the value-pairing fix in PR #13. A static list would have multiplied the failure modes.
- `which`/`where` shell-out (Key Technical Decisions §3) — works as designed; the cross-reviewer security take in PR #13
noted hostile-PATH redirection as a low-risk residual but recommended the `which` crate as a future improvement, not a
blocker.
- `Project::discover()` already handles file paths (Key Technical Decisions §4) — true, no new constructor needed.

### Test parity

| Stage | Unit | Integration | Notes |
|-------|------|-------------|-------|
| Pre-Plan 003 baseline | 233 | 12 | from commit `45b5234` |
| After PR #12 (initial impl) | 244 | 26 | +11 unit, +14 integration |
| After PR #13 (edge-case fixes) | 253 | 34 | +9 unit, +8 integration |

### Plan 002 coordination

The completions regeneration noted in System-Wide Impact happened twice (once per PR); both PRs commit the regenerated
`completions/anc.{bash,zsh,fish,elvish,powershell}` files. No separate Plan 002 step needed for this plan's completion
deltas.

### Solutions-docs follow-up

The full pattern (with all seven gotchas, before/after code, and the working invocation matrix) was compounded into:

- `~/dev/solutions-docs/best-practices/clap-default-subcommand-via-argv-pre-parse-20260415.md`

Future Rust CLIs in this orbit that want a default subcommand should read that doc before reimplementing — the cluster
of edge cases is the kind of footgun that's much cheaper to avoid than rediscover.
31 changes: 30 additions & 1 deletion scripts/generate-completions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ else
fi

# Helper to invoke the detected completions interface
gen() {
gen_raw() {
local shell="$1"
if [[ "$COMP_STYLE" == "subcommand" ]]; then
"$BINARY" completions "$shell"
Expand All @@ -74,6 +74,35 @@ gen() {
fi
}

# Project-specific post-processing. clap_complete's bash backend ignores
# ValueHint::CommandName and emits `compgen -f` for every value-taking flag.
# zsh/fish honour the hint correctly. We patch bash so `--command <TAB>` and
# any other CommandName flags suggest PATH commands. Update the list when
# adding new flags with that hint.
#
# Reads from stdin, writes to stdout, no-op for non-bash shells.
post_process() {
local shell="$1"
if [[ "$shell" != "bash" ]]; then
cat
return
fi
local command_name_flags=("--command")
local sed_program=""
for flag in "${command_name_flags[@]}"; do
sed_program+=$'\n'"/^[[:space:]]*${flag})[[:space:]]*$/,/;;/{ s|compgen -f|compgen -c|; }"
done
if [[ -n "$sed_program" ]]; then
sed "$sed_program"
else
cat
fi
}

gen() {
gen_raw "$1" | post_process "$1"
}

if [[ "$MODE" == "check" ]]; then
STALE=0
for shell in "${SHELLS[@]}"; do
Expand Down
Loading