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
6 changes: 3 additions & 3 deletions cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func agentCreate(args []string, templates embed.FS) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd agent create NAME --description '...'")
render.Err("usage: " + prog() + " agent create NAME --description '...'")
return 1
}
opts.Name = positionals[0]
Expand Down Expand Up @@ -176,7 +176,7 @@ func agentShow(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd agent show NAME")
render.Err("usage: " + prog() + " agent show NAME")
return 1
}
r, err := workspace.Resolve(root)
Expand Down Expand Up @@ -206,7 +206,7 @@ func agentDelete(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd agent delete NAME --force")
render.Err("usage: " + prog() + " agent delete NAME --force")
return 1
}
if !force {
Expand Down
53 changes: 33 additions & 20 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,29 @@ func normalizeLeadingGlobalFlags(args []string) ([]string, error) {
return args, nil
}

// prog is the program name echoed in help text and usage/guidance messages.
// The npm launcher sets CSDD_PROG to "npx @protonspy/csdd" when csdd is run via
// npx, so the help reflects the exact command the user typed; a global install
// or a plain `go`-built binary falls back to the bare name "csdd".
func prog() string {
if p := os.Getenv("CSDD_PROG"); p != "" {
return p
}
return "csdd"
}

func help(w *os.File) {
fmt.Fprintln(w, strings.TrimSpace(helpText))
fmt.Fprintln(w, strings.TrimSpace(helpText()))
}

const helpText = `
func helpText() string {
return fmt.Sprintf(`
csdd — manage Claude Code workflow artifacts (steering, specs, skills, custom agents).

USAGE
csdd Launch the interactive TUI.
csdd tui Launch the TUI explicitly.
csdd <resource> <action> [flags]
%[1]s Launch the interactive TUI.
%[1]s tui Launch the TUI explicitly.
%[1]s <resource> <action> [flags]

RESOURCES
init Bootstrap a Claude Code workspace.
Expand All @@ -122,25 +134,26 @@ GLOBAL FLAGS
-v, --version Show version.

EXAMPLES
csdd init --with-baseline
csdd steering create api-conventions \
%[1]s init --with-baseline
%[1]s steering create api-conventions \
--inclusion fileMatch --pattern 'src/api/**/*' --pattern '**/*Controller.*'
csdd steering create observability \
%[1]s steering create observability \
--inclusion auto --description 'Logging/metrics. Use when adding instrumentation.'
csdd spec init photo-albums
csdd spec generate photo-albums --artifact requirements
csdd spec approve photo-albums --phase requirements
csdd skill create spec-tasks \
%[1]s spec init photo-albums
%[1]s spec generate photo-albums --artifact requirements
%[1]s spec approve photo-albums --phase requirements
%[1]s skill create spec-tasks \
--description 'Generate tasks.md with boundary/depends annotations.' # .claude/skills/
csdd agent create code-reviewer \
%[1]s agent create code-reviewer \
--description 'Read-only adversarial reviewer' --tools Read --tools Grep # .claude/agents/
csdd mcp add filesystem \
%[1]s mcp add filesystem \
--command npx --arg -y --arg '@modelcontextprotocol/server-filesystem' --arg . # .mcp.json
csdd mcp add linear --url https://mcp.linear.app/mcp --type http
csdd mcp validate
csdd export kiro # .kiro/steering + .kiro/specs
csdd export codex --out ./build # AGENTS.md + .codex/config.toml
`
%[1]s mcp add linear --url https://mcp.linear.app/mcp --type http
%[1]s mcp validate
%[1]s export kiro # .kiro/steering + .kiro/specs
%[1]s export codex --out ./build # AGENTS.md + .codex/config.toml
`, prog())
}

// parseFlags wraps fs.Parse to allow positional arguments to appear before
// flags (e.g., `csdd steering create api-conventions --inclusion always`).
Expand Down Expand Up @@ -189,7 +202,7 @@ func addForce(fs *flag.FlagSet, dst *bool) {
// parseAction extracts the action subcommand and returns the remaining args.
func parseAction(resource string, args []string) (string, []string, error) {
if len(args) == 0 {
return "", nil, fmt.Errorf("missing action for `csdd %s`", resource)
return "", nil, fmt.Errorf("missing action for `%s %s`", prog(), resource)
}
return args[0], args[1:], nil
}
Expand Down
30 changes: 30 additions & 0 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,36 @@ func TestHelpAndVersion(t *testing.T) {
}
}

// TestProgName covers the dynamic program name: help and usage strings echo the
// bare binary name by default, but switch to whatever CSDD_PROG holds (the npm
// launcher sets it to "npx @protonspy/csdd" for npx invocations).
func TestProgName(t *testing.T) {
t.Run("default is bare csdd", func(t *testing.T) {
t.Setenv("CSDD_PROG", "")
_, help, _ := run(t, "--help")
if !strings.Contains(help, "csdd spec generate") {
t.Errorf("help should show bare csdd commands, got %q", help)
}
if strings.Contains(help, "npx @protonspy/csdd") {
t.Errorf("help should not show the npx prefix by default, got %q", help)
}
if _, _, errOut := run(t, "spec", "init"); !strings.Contains(errOut, "usage: csdd spec init") {
t.Errorf("usage should show bare csdd, got %q", errOut)
}
})

t.Run("CSDD_PROG is echoed verbatim", func(t *testing.T) {
t.Setenv("CSDD_PROG", "npx @protonspy/csdd")
_, help, _ := run(t, "--help")
if !strings.Contains(help, "npx @protonspy/csdd spec generate") {
t.Errorf("help should echo CSDD_PROG, got %q", help)
}
if _, _, errOut := run(t, "spec", "init"); !strings.Contains(errOut, "usage: npx @protonspy/csdd spec init") {
t.Errorf("usage should echo CSDD_PROG, got %q", errOut)
}
})
}

func TestUnknownResource(t *testing.T) {
code, _, errOut := run(t, "nonsense")
if code == 0 {
Expand Down
2 changes: 1 addition & 1 deletion cmd/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func runExport(args []string) int {
target, rest, err := parseAction("export", args)
if err != nil {
render.Err(err.Error())
render.Info("usage: csdd export {kiro|codex} [--out DIR] [--force]")
render.Info("usage: " + prog() + " export {kiro|codex} [--out DIR] [--force]")
return 1
}
fs := flag.NewFlagSet("export "+target, flag.ContinueOnError)
Expand Down
2 changes: 1 addition & 1 deletion cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func runInit(args []string, templates embed.FS) int {
offerGitignore(root)
render.Info("Enable the pre-push test gate: `git config core.hooksPath .githooks`")
if !withBaseline {
render.Info("Run `csdd steering init` to scaffold standard steering files.")
render.Info("Run `" + prog() + " steering init` to scaffold standard steering files.")
}
return 0
}
Expand Down
8 changes: 4 additions & 4 deletions cmd/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func mcpAdd(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd mcp add NAME (--command CMD [--arg A]... | --url URL [--type sse|http]) [--env K=V]... [--disabled] [--force]")
render.Err("usage: " + prog() + " mcp add NAME (--command CMD [--arg A]... | --url URL [--type sse|http]) [--env K=V]... [--disabled] [--force]")
return 1
}
opts.Name = positionals[0]
Expand Down Expand Up @@ -320,7 +320,7 @@ func mcpRemove(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd mcp remove NAME --force")
render.Err("usage: " + prog() + " mcp remove NAME --force")
return 1
}
name := positionals[0]
Expand Down Expand Up @@ -369,7 +369,7 @@ func mcpToggle(args []string, disabled bool) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd mcp " + verb + " NAME")
render.Err("usage: " + prog() + " mcp " + verb + " NAME")
return 1
}
name := positionals[0]
Expand Down Expand Up @@ -496,7 +496,7 @@ func mcpResolveLoadNamed(args []string, action string) (mcpResult, string, int)
return res, "", code
}
if len(positionals) < 1 {
render.Err("usage: csdd mcp " + action + " NAME")
render.Err("usage: " + prog() + " mcp " + action + " NAME")
return res, "", 1
}
return res, positionals[0], 0
Expand Down
10 changes: 5 additions & 5 deletions cmd/skill.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func skillCreate(args []string, templates embed.FS) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd skill create NAME --description '...'")
render.Err("usage: " + prog() + " skill create NAME --description '...'")
return 1
}
opts.Name = positionals[0]
Expand Down Expand Up @@ -179,7 +179,7 @@ func skillShow(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd skill show NAME")
render.Err("usage: " + prog() + " skill show NAME")
return 1
}
r, err := workspace.Resolve(root)
Expand Down Expand Up @@ -217,7 +217,7 @@ func skillAddArtifact(args []string, subdir string) int {
return failOnFlagParse(err)
}
if len(positionals) < 2 {
render.Err("usage: csdd skill add-" + subdir + " SKILL FILE")
render.Err("usage: " + prog() + " skill add-" + subdir + " SKILL FILE")
return 1
}
r, err := workspace.Resolve(root)
Expand Down Expand Up @@ -304,7 +304,7 @@ func skillValidate(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd skill validate NAME")
render.Err("usage: " + prog() + " skill validate NAME")
return 1
}
r, err := workspace.Resolve(root)
Expand Down Expand Up @@ -340,7 +340,7 @@ func skillDelete(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd skill delete NAME --force")
render.Err("usage: " + prog() + " skill delete NAME --force")
return 1
}
if !force {
Expand Down
18 changes: 9 additions & 9 deletions cmd/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func specInit(args []string, templates embed.FS) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd spec init FEATURE")
render.Err("usage: " + prog() + " spec init FEATURE")
return 1
}
r, err := workspace.Resolve(root)
Expand Down Expand Up @@ -113,7 +113,7 @@ func specInit(args []string, templates embed.FS) int {
return 1
}
render.OK("created " + workspace.Relative(r, target) + "/")
render.Info("next: `csdd spec generate <feature> --artifact requirements`")
render.Info(fmt.Sprintf("next: `%s spec generate <feature> --artifact requirements`", prog()))
return 0
}

Expand Down Expand Up @@ -192,7 +192,7 @@ func specShow(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd spec show FEATURE")
render.Err("usage: " + prog() + " spec show FEATURE")
return 1
}
r, err := workspace.Resolve(root)
Expand Down Expand Up @@ -276,7 +276,7 @@ func specGenerate(args []string, templates embed.FS) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 || opts.Artifact == "" {
render.Err("usage: csdd spec generate FEATURE --artifact {requirements|design|tasks|research|bugfix}")
render.Err("usage: " + prog() + " spec generate FEATURE --artifact {requirements|design|tasks|research|bugfix}")
return 1
}
opts.Feature = positionals[0]
Expand All @@ -298,7 +298,7 @@ func SpecGenerate(templates embed.FS, opts SpecGenerateOptions) error {
}
sdir := filepath.Join(paths.Specs(r), opts.Feature)
if !pathExists(sdir) {
return fmt.Errorf("spec not found: %s. Run `csdd spec init %s` first", opts.Feature, opts.Feature)
return fmt.Errorf("spec not found: %s. Run `%s spec init %s` first", opts.Feature, prog(), opts.Feature)
}
data, err := loadSpecJSON(sdir)
if err != nil {
Expand Down Expand Up @@ -381,7 +381,7 @@ func specApprove(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 || opts.Phase == "" {
render.Err("usage: csdd spec approve FEATURE --phase {requirements|design|tasks}")
render.Err("usage: " + prog() + " spec approve FEATURE --phase {requirements|design|tasks}")
return 1
}
opts.Feature = positionals[0]
Expand Down Expand Up @@ -473,7 +473,7 @@ func specValidate(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd spec validate FEATURE")
render.Err("usage: " + prog() + " spec validate FEATURE")
return 1
}
r, err := workspace.Resolve(root)
Expand Down Expand Up @@ -519,7 +519,7 @@ func validationScope(specDir string, data SpecJSON) (validator.Phase, []validato
}
return validator.PhaseAll, []validator.Issue{{
File: "spec.json",
Msg: "no generated artifacts to validate; run `csdd spec generate <feature> --artifact requirements` first",
Msg: fmt.Sprintf("no generated artifacts to validate; run `%s spec generate <feature> --artifact requirements` first", prog()),
}}
}

Expand All @@ -534,7 +534,7 @@ func specDelete(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd spec delete FEATURE --force")
render.Err("usage: " + prog() + " spec delete FEATURE --force")
return 1
}
feature := positionals[0]
Expand Down
6 changes: 3 additions & 3 deletions cmd/steering.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func steeringCreate(args []string, templates embed.FS) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd steering create NAME --inclusion {always|fileMatch|manual|auto} [...]")
render.Err("usage: " + prog() + " steering create NAME --inclusion {always|fileMatch|manual|auto} [...]")
return 1
}
opts.Name = positionals[0]
Expand Down Expand Up @@ -265,7 +265,7 @@ func steeringShow(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd steering show NAME")
render.Err("usage: " + prog() + " steering show NAME")
return 1
}
r, err := workspace.Resolve(root)
Expand Down Expand Up @@ -299,7 +299,7 @@ func steeringDelete(args []string) int {
return failOnFlagParse(err)
}
if len(positionals) < 1 {
render.Err("usage: csdd steering delete NAME")
render.Err("usage: " + prog() + " steering delete NAME")
return 1
}
name := positionals[0]
Expand Down
6 changes: 3 additions & 3 deletions internal/templater/templates/guides/claude-code-sdd.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Skills, agent prompts, and steering content SHALL be grounded in real expertise

## 4. Repository structure

Every project adopting this workflow SHALL have the following layout, scaffolded by `csdd init`.
Every project adopting this workflow SHALL have the following layout, scaffolded by `npx @protonspy/csdd init`.

```text
repo/
Expand Down Expand Up @@ -269,7 +269,7 @@ In Claude Code, steering is loaded as **always-on project memory via `@`-imports

Every imported file loads **always**, on every session — there is no native conditional `fileMatch`/`auto` loading of arbitrary markdown in Claude Code.

> **IMPORTANT semantic note on inclusion modes.** `csdd steering create --inclusion {always|fileMatch|auto|manual}` still exists, and the `inclusion:` / `fileMatchPattern:` / `name:` / `description:` frontmatter is still produced and still validated. Under Claude Code, however, this frontmatter is **advisory metadata documenting the file's intended scope** — it is *not* a runtime loading mechanism. Everything imported into `CLAUDE.md` loads always, regardless of `inclusion:` value. Use the frontmatter to communicate intent to humans and to keep portability with other tools; do not expect `fileMatch` to change what Claude Code actually loads. If you want a steering file to load only on demand, do not import it into `CLAUDE.md` and instead reference it explicitly with `@.claude/steering/<file>.md` in a prompt when you need it.
> **IMPORTANT semantic note on inclusion modes.** `npx @protonspy/csdd steering create --inclusion {always|fileMatch|auto|manual}` still exists, and the `inclusion:` / `fileMatchPattern:` / `name:` / `description:` frontmatter is still produced and still validated. Under Claude Code, however, this frontmatter is **advisory metadata documenting the file's intended scope** — it is *not* a runtime loading mechanism. Everything imported into `CLAUDE.md` loads always, regardless of `inclusion:` value. Use the frontmatter to communicate intent to humans and to keep portability with other tools; do not expect `fileMatch` to change what Claude Code actually loads. If you want a steering file to load only on demand, do not import it into `CLAUDE.md` and instead reference it explicitly with `@.claude/steering/<file>.md` in a prompt when you need it.

### 5.2 Foundational files

Expand Down Expand Up @@ -2126,7 +2126,7 @@ Do not change production implementation without asking for confirmation.

### Phase 1 — Foundations (week 1)

- [ ] Run `csdd init` to scaffold `CLAUDE.md`, `.claude/`, `specs/`, rules, templates, skills, and this guide.
- [ ] Run `npx @protonspy/csdd init` to scaffold `CLAUDE.md`, `.claude/`, `specs/`, rules, templates, skills, and this guide.
- [ ] Configure `.gitignore` and permission deny rules for protected paths.
- [ ] Generate foundational steering: `product.md`, `tech.md`, `structure.md`; confirm they are imported into `CLAUDE.md`.
- [ ] Configure required MCP servers at the correct scope (project vs user).
Expand Down
6 changes: 3 additions & 3 deletions internal/templater/templates/root/CLAUDE.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ between the markers.

## Workflow at a glance

1. `csdd spec init <feature>` then iterate requirements → design → tasks.
1. `npx @protonspy/csdd spec init <feature>` then iterate requirements → design → tasks.
2. Each phase requires explicit human approval recorded in `spec.json`.
3. `ready_for_implementation` flips to `true` only after all three phases are approved.
4. Implementation runs one task per iteration with fresh-context sub-agents (implementer → reviewer → debugger), TDD red→green per task.
Expand All @@ -51,5 +51,5 @@ between the markers.
## Reference

- Operational guide for agents: [`csdd.md`](./csdd.md)
- Canonical spec: `docs/guides/claude-code-sdd.md` — the self-contained SDD+TDD guide (scaffolded by `csdd init`)
- CLI help: `csdd --help`
- Canonical spec: `docs/guides/claude-code-sdd.md` — the self-contained SDD+TDD guide (scaffolded by `npx @protonspy/csdd init`)
- CLI help: `npx @protonspy/csdd --help`
Loading
Loading