diff --git a/CLAUDE.md b/CLAUDE.md index 5ec2744..a183109 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ SpecSync uses OpenSpec and itself (dogfooding) to manage development. - **Implementation**: Changes reference GitHub issues synced by specsync - **Dogfooding**: This repo dogfoods specsync; every issue is spec-generated - **CI gates**: Changelog linking, archive hygiene, task accuracy enforced -- **Agent tools**: `specsync agent-help`, `specsync doctor`, `specsync changes --json` +- **Agent tools**: `specsync agent-help`, `specsync doctor`, `specsync changes -json` ## Where to Go Next diff --git a/README.md b/README.md index 0266722..2d20626 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,6 @@ specsync [sync] # project changes -> issues (default command) specsync pull # pull an issue into a local change specsync scan # what already exists in an area? specsync trace # print the raw spec<->commit<->issue link graph -specsync relate # print the work graph around a change specsync link # cross-link two or more changes specsync spinoff # spawn emergent work as a linked sibling specsync epic # create a coordination issue and wire children @@ -471,7 +470,7 @@ See what you've captured that nobody has triaged yet: ```bash specsync ideas # table output, oldest first -specsync ideas --json # JSON for scripting +specsync ideas -json # JSON for scripting ``` ### Triage: pull → triage → act @@ -492,21 +491,6 @@ specsync trace -change my-feature # scope to one change specsync trace -since v0.3.0 -json # commits since a tag, as JSON ``` -### `relate` — the work graph for planning - -Prints the connected slice around a change: related specs (via `links.md`), -their issues, linked PRs, merge commits, and releases. Built on the -**asserted-graph principle**: every edge is authored in `links.md`/markers or -read as fact from `git`/`gh` — never inferred or probabilistic. - -```bash -specsync relate -change my-feature # graph around one change -specsync relate -path src/auth.ts # changes touching a file -``` - -Read-only: never writes to disk or the tracker. Supports delta annotations -when `openspec` is on PATH (tags spec nodes with ADDED/MODIFIED/REMOVED counts). - **Non-goals** (parked for future changes): - **`graph.json` export** — no consumer exists yet - **Graphify / inferred edges** — code-symbol edges would pollute an asserted graph diff --git a/WORKFLOW.md b/WORKFLOW.md index 0c61041..dedbdd2 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -223,7 +223,7 @@ If `specsync verify` warns about a PR missing a reference, add one before mergin This should not happen. If it does: 1. Check if `.specsync/board.json` exists and is readable -2. Verify human-move detection ran: `specsync sync --dry-run` should show StatusSkipped +2. Verify human-move detection ran: `specsync sync -dry-run` should show StatusSkipped 3. If not skipping, the prior base state may be stale; delete `.specsync/board.json` and re-sync ### "My priority/stage isn't being used" diff --git a/cmd/specsync/agent_help.go b/cmd/specsync/agent_help.go index ec3d56e..d175bf4 100644 --- a/cmd/specsync/agent_help.go +++ b/cmd/specsync/agent_help.go @@ -92,7 +92,7 @@ var commandMetadata = map[string]AgentCommandHelp{ Examples: []string{ "specsync -dry-run -change my-change", "specsync -change my-change", - "specsync sync --json -change my-change", + "specsync sync -json -change my-change", }, }, "pull": { @@ -139,7 +139,7 @@ var commandMetadata = map[string]AgentCommandHelp{ Examples: []string{ "specsync pull -issue 42 -dry-run", "specsync pull -issue 42", - "specsync pull -issue 42 --json", + "specsync pull -issue 42 -json", }, }, "link": { @@ -195,7 +195,7 @@ var commandMetadata = map[string]AgentCommandHelp{ }, Examples: []string{ "specsync scan cmd/specsync/ 'label creation'", - "specsync scan cmd/specsync/ --json", + "specsync scan cmd/specsync/ -json", "specsync scan openspec/changes/ reconcile", }, }, @@ -229,7 +229,7 @@ var commandMetadata = map[string]AgentCommandHelp{ Examples: []string{ "specsync trace", "specsync trace -change my-change", - "specsync trace --json", + "specsync trace -json", }, }, "changes": { @@ -262,7 +262,7 @@ var commandMetadata = map[string]AgentCommandHelp{ Examples: []string{ "specsync changes", "specsync changes --stage backlog", - "specsync changes --json", + "specsync changes -json", }, }, "set-stage": { @@ -327,7 +327,7 @@ var commandMetadata = map[string]AgentCommandHelp{ Examples: []string{ "specsync release-plan", "specsync release-plan --since v0.9.0", - "specsync release-plan --json", + "specsync release-plan -json", }, }, "changelog": { @@ -423,7 +423,7 @@ var commandMetadata = map[string]AgentCommandHelp{ }, Examples: []string{ "specsync audit", - "specsync audit --json", + "specsync audit -json", }, }, "audit-tasks": { @@ -450,7 +450,7 @@ var commandMetadata = map[string]AgentCommandHelp{ }, Examples: []string{ "specsync audit-tasks", - "specsync audit-tasks --json", + "specsync audit-tasks -json", }, }, "validate": { @@ -477,7 +477,7 @@ var commandMetadata = map[string]AgentCommandHelp{ }, Examples: []string{ "specsync validate", - "specsync validate --json", + "specsync validate -json", }, }, "spinoff": { @@ -566,13 +566,258 @@ var commandMetadata = map[string]AgentCommandHelp{ "specsync install-skill --claude-code --profile docs", }, }, + "doctor": { + Command: "doctor", + Description: "Diagnose skill installation, environment, and context health.", + Mutates: false, + Workflow: AgentCommandWorkflow{ + Position: "anytime", + RelatedBefore: []string{}, + RelatedAfter: []string{"install-skill"}, + }, + Flags: []AgentCommandFlag{ + { + Name: "json", + Type: "boolean", + Required: false, + Default: false, + Description: "Emit machine-readable JSON output", + }, + { + Name: "skip-skill-update", + Type: "boolean", + Required: false, + Default: false, + Description: "Skip auto-updating skill files before diagnosing", + }, + }, + SafetyRules: []string{ + "Read-only operation", + }, + Examples: []string{ + "specsync doctor", + "specsync doctor install -json", + "specsync doctor context -json", + }, + }, + "epic": { + Command: "epic", + Description: "Create a coordination issue and wire children to it.", + Mutates: true, + Workflow: AgentCommandWorkflow{ + Position: "plan", + RelatedBefore: []string{"changes"}, + RelatedAfter: []string{"sync"}, + }, + Flags: []AgentCommandFlag{ + { + Name: "repo", + Type: "string", + Required: false, + Default: "auto-detect", + Description: "Target repo for the epic issue, as owner/name", + }, + { + Name: "child", + Type: "string", + Required: false, + Description: "A child to attach: local change slug, owner/repo#N, bare #N, or issue URL (repeatable)", + }, + { + Name: "dry-run", + Type: "boolean", + Required: false, + Default: false, + Description: "Print what would happen without creating or editing any issue", + }, + }, + SafetyRules: []string{ + "Use -dry-run to preview before creating or editing issues", + "Re-running with the same title converges onto the existing epic instead of duplicating it", + }, + Examples: []string{ + "specsync epic 'Q3 auth rework' -child my-change -child other-org/other-repo#42 -dry-run", + "specsync epic 'Q3 auth rework' -child my-change", + }, + }, + "idea": { + Command: "idea", + Description: "Capture a free-text idea as a stage:intake issue.", + Mutates: true, + Workflow: AgentCommandWorkflow{ + Position: "capture", + RelatedBefore: []string{}, + RelatedAfter: []string{"ideas", "pull"}, + }, + Flags: []AgentCommandFlag{ + { + Name: "repo", + Type: "string", + Required: false, + Default: "config ideas_repo, $SPECSYNC_IDEAS_REPO, or current repo", + Description: "Repo to create the idea issue in, as owner/name", + }, + }, + SafetyRules: []string{ + "Creates a real GitHub issue — there is no -dry-run", + }, + Examples: []string{ + "specsync idea 'Consider caching the trace graph'", + "echo 'Consider caching the trace graph' | specsync idea", + }, + }, + "ideas": { + Command: "ideas", + Description: "List open stage:intake issues for the repo.", + Mutates: false, + Workflow: AgentCommandWorkflow{ + Position: "review", + RelatedBefore: []string{}, + RelatedAfter: []string{"pull"}, + }, + Flags: []AgentCommandFlag{ + { + Name: "repo", + Type: "string", + Required: false, + Default: "config ideas_repo, $SPECSYNC_IDEAS_REPO, or current repo", + Description: "Repo to list idea issues from, as owner/name", + }, + { + Name: "json", + Type: "boolean", + Required: false, + Default: false, + Description: "Emit machine-readable JSON output", + }, + }, + SafetyRules: []string{ + "Read-only operation", + }, + Examples: []string{ + "specsync ideas", + "specsync ideas -json", + }, + }, + "archive": { + Command: "archive", + Description: "Archive a completed change: final push, close, then retention.", + Mutates: true, + Workflow: AgentCommandWorkflow{ + Position: "after-complete", + RelatedBefore: []string{"changes", "sync"}, + RelatedAfter: []string{"verify"}, + }, + Flags: []AgentCommandFlag{ + { + Name: "change", + Type: "string", + Required: true, + Description: "Change to archive", + }, + { + Name: "repo", + Type: "string", + Required: false, + Default: "auto-detect", + Description: "Target repo as owner/name", + }, + { + Name: "retain", + Type: "string", + Required: false, + Description: "Retention policy: move (keep) or prune (delete)", + }, + { + Name: "force", + Type: "boolean", + Required: false, + Default: false, + Description: "Archive even when tasks are unchecked", + }, + { + Name: "dry-run", + Type: "boolean", + Required: false, + Default: false, + Description: "Print the plan without making changes", + }, + }, + SafetyRules: []string{ + "Use -dry-run to preview before archiving", + "Refuses unchecked-task changes unless -force is set", + }, + Examples: []string{ + "specsync archive -change my-change -dry-run", + "specsync archive -change my-change -retain move", + }, + }, + "set-priority": { + Command: "set-priority", + Description: "Set or unset a change's numeric priority override.", + Mutates: true, + Workflow: AgentCommandWorkflow{ + Position: "plan", + RelatedBefore: []string{"changes"}, + RelatedAfter: []string{"changes"}, + }, + Flags: []AgentCommandFlag{ + { + Name: "openspec", + Type: "string", + Required: false, + Default: "openspec", + Description: "Path to the openspec/ directory", + }, + }, + SafetyRules: []string{ + "Takes positional args: <1-100|unset>, not flags", + }, + Examples: []string{ + "specsync set-priority my-change 80", + "specsync set-priority my-change unset", + }, + }, + "note": { + Command: "note", + Description: "Append a discovery line to a change's discoveries.md.", + Mutates: true, + Workflow: AgentCommandWorkflow{ + Position: "implementation", + RelatedBefore: []string{}, + RelatedAfter: []string{"spinoff"}, + }, + Flags: []AgentCommandFlag{ + { + Name: "openspec", + Type: "string", + Required: false, + Default: "openspec", + Description: "Path to the openspec/ directory", + }, + { + Name: "dry-run", + Type: "boolean", + Required: false, + Default: false, + Description: "Show what would be written without modifying files", + }, + }, + SafetyRules: []string{ + "Takes positional args: , not a -text flag", + }, + Examples: []string{ + "specsync note my-change 'Found edge case in X' -dry-run", + "specsync note my-change 'Found edge case in X'", + }, + }, } // runAgentHelp handles the agent-help command. func runAgentHelp(args []string) { fs := flag.NewFlagSet("agent-help", flag.ExitOnError) jsonFlag := fs.Bool("json", false, "emit machine-readable JSON output") - if err := fs.Parse(args); err != nil { + if err := fs.Parse(reorderFlagsFirst(args)); err != nil { fail(err) } @@ -632,8 +877,8 @@ func renderAgentHelpOverview(asJSON bool) { "commands": availableCommands(), "tips": []string{ "Use `specsync agent-help ` for detailed help on any command", - "Use `specsync agent-help --json` for machine-readable output", - "All output formats support --json for automation", + "Use `specsync agent-help -json` for machine-readable output", + "All output formats support -json for automation", "Always use -dry-run before making changes", }, } @@ -649,7 +894,7 @@ SpecSync manages OpenSpec changes and keeps them in sync with GitHub Issues. For help on any command, use: specsync agent-help - specsync agent-help --json # for JSON output + specsync agent-help -json # for JSON output Available commands:`) for _, cmd := range availableCommands() { @@ -662,7 +907,7 @@ Available commands:`) ## Tips - Always use -dry-run before making changes -- Many commands support --json for machine-readable output +- Many commands support -json for machine-readable output - See agent-help for full details and flags - Read AGENTS.md for workflow patterns `) diff --git a/cmd/specsync/doctor.go b/cmd/specsync/doctor.go index a1b981a..86e5140 100644 --- a/cmd/specsync/doctor.go +++ b/cmd/specsync/doctor.go @@ -88,7 +88,7 @@ func runDoctor(args []string) { fs := flag.NewFlagSet("doctor", flag.ExitOnError) jsonFlag := fs.Bool("json", false, "emit machine-readable JSON output") skipUpdate := fs.Bool("skip-skill-update", false, "skip auto-updating skill files") - if err := fs.Parse(args); err != nil { + if err := fs.Parse(reorderFlagsFirst(args)); err != nil { fail(err) } diff --git a/cmd/specsync/main.go b/cmd/specsync/main.go index 2fbdda5..81651fb 100644 --- a/cmd/specsync/main.go +++ b/cmd/specsync/main.go @@ -31,7 +31,7 @@ var knownSubcommands = map[string]bool{ "release-plan": true, "changelog": true, "install-skill": true, "changes": true, "set-stage": true, "set-priority": true, "note": true, "sync": true, "audit": true, "audit-tasks": true, "validate": true, - "spinoff": true, "pr-body": true, "verify": true, "relate": true, "work-graph": true, + "spinoff": true, "pr-body": true, "verify": true, "agent-help": true, "doctor": true, "idea": true, "ideas": true, "archive": true, "epic": true, } @@ -94,7 +94,7 @@ func deprecatedSlugFlag(args []string) error { func main() { cmd, rest, err := resolveSubcommand(os.Args[1:]) if err != nil { - fmt.Fprintf(os.Stderr, "specsync: %v\n\nRun with no subcommand (optionally with flags) to sync, or use one of: pull, link, scan, trace, release-plan, changelog, install-skill, changes, set-stage, set-priority, note, audit, audit-tasks, validate, spinoff, pr-body, verify, relate, work-graph, idea, ideas, archive, epic\n", err) + fmt.Fprintf(os.Stderr, "specsync: %v\n\nRun with no subcommand (optionally with flags) to sync, or use one of: pull, link, scan, trace, release-plan, changelog, install-skill, changes, set-stage, set-priority, note, audit, audit-tasks, validate, spinoff, pr-body, verify, idea, ideas, archive, epic\n", err) os.Exit(2) } @@ -179,6 +179,25 @@ func containsFlag(args []string, name string) bool { return false } +// reorderFlagsFirst moves every flag token (anything starting with "-") ahead +// of positional args, preserving relative order within each group. Go's flag +// package stops parsing at the first positional token, so a flag written +// after a subcommand name (e.g. "agent-help sync -json") is otherwise left +// unparsed. Only safe for all-boolean flag sets, where no flag consumes a +// separate value token. +func reorderFlagsFirst(args []string) []string { + flags := make([]string, 0, len(args)) + positional := make([]string, 0, len(args)) + for _, arg := range args { + if strings.HasPrefix(arg, "-") { + flags = append(flags, arg) + } else { + positional = append(positional, arg) + } + } + return append(flags, positional...) +} + // stringSlice implements flag.Value for repeatable -provider flags. type stringSlice []string diff --git a/openspec/changes/cli-help-consistency-fixes/proposal.md b/openspec/changes/cli-help-consistency-fixes/proposal.md new file mode 100644 index 0000000..c7ab1c1 --- /dev/null +++ b/openspec/changes/cli-help-consistency-fixes/proposal.md @@ -0,0 +1,79 @@ +# CLI help and flag-parsing consistency fixes + +## Context + +A user asked whether specsync's `-flag` vs `--flag` help text was internally +consistent, and whether all documented commands actually work. Auditing the +CLI surfaced one real safety bug and a few smaller correctness/consistency +gaps in the `agent-help`/`doctor` machinery, on top of confirming that Go's +`flag` package genuinely treats `-x`/`--x` identically (not a bug). + +## Issues Found + +### 1. `specsync relate` silently ran a live `sync` instead of erroring + +`relate`/`work-graph` were still listed in `knownSubcommands` and the +unknown-subcommand hint text in `cmd/specsync/main.go`, and README documented +`specsync relate -change my-feature` as a safe read-only example with no +`-dry-run`. But `d07979d0` had deliberately removed the dispatch `case +"relate"` (pending the not-yet-implemented `work-graph` change), leaving the +command fall through to `default: runSync(rest)`. Following the README +example verbatim, without `-dry-run`, would trigger a real GitHub sync +instead of printing a graph. + +### 2. `agent-help --json` / `doctor --json` ignored `--json` in that order + +Go's `flag.Parse` stops at the first positional token, so a flag written +after the subcommand name was silently dropped — the exact order the tool's +own help text told people to use (`agent-help --json`). Only +`agent-help --json ` (flag-first) actually worked. + +### 3. 9 real subcommands missing from `agent-help` + +`doctor`, `epic`, `idea`, `ideas`, `archive`, `set-priority`, `note` all +dispatch and work, but `agent-help ` for any of them returned "unknown +command," and the top-level `agent-help` overview only listed 16 of the ~25 +real subcommands. + +### 4. Mixed `-flag`/`--flag` style in docs and help text + +`WORKFLOW.md`, `README.md`, and `CLAUDE.md` each had one example using +`--flag` where the surrounding docs consistently use `-flag`. `agent-help`'s +own embedded examples and tips mixed both styles for the same flag +(`-dry-run` in one tip, `--json` in the next). + +## Proposed Changes + +1. Remove `relate`/`work-graph` from `knownSubcommands` and the + unknown-subcommand hint in `main.go`; drop the now-false `relate` + documentation from README. `relate.go` is left in place, unwired, for + whenever the `work-graph` change actually ships. +2. Add a `reorderFlagsFirst` helper and use it in `runAgentHelp` and + `runDoctor` so `-json`/`--json` parses correctly regardless of position + relative to the subcommand name. +3. Add `commandMetadata` entries for `doctor`, `epic`, `idea`, `ideas`, + `archive`, `set-priority`, `note` in `agent_help.go`. +4. Standardize on single-dash (`-flag`) in `WORKFLOW.md`, `README.md`, + `CLAUDE.md`, and every example/tip string in `agent_help.go`. + +## Impact + +**Severity**: Medium (item 1 is a real footgun for anyone following the +README's own `relate` example; the rest are correctness/discoverability +cleanups with no functional risk). + +- **Affected**: `cmd/specsync/main.go`, `cmd/specsync/doctor.go`, + `cmd/specsync/agent_help.go`, `README.md`, `WORKFLOW.md`, `CLAUDE.md` +- **No breaking changes**: no flag or subcommand behavior changes for any + command that was already working correctly; `relate`/`work-graph` were + already effectively broken (silently doing the wrong thing), so removing + them from the menus is a strict improvement, not a regression. + +## Release Notes + +Fixed `specsync relate` silently running a live sync instead of erroring +(it was already unwired; now it fails loudly like any other unknown +subcommand). Fixed `agent-help`/`doctor` ignoring `--json` when placed after +the subcommand name. Added `agent-help` entries for `doctor`, `epic`, +`idea`, `ideas`, `archive`, `set-priority`, and `note`. Standardized CLI +help text and docs on single-dash flag style throughout. diff --git a/openspec/changes/cli-help-consistency-fixes/tasks.md b/openspec/changes/cli-help-consistency-fixes/tasks.md new file mode 100644 index 0000000..fde7b1c --- /dev/null +++ b/openspec/changes/cli-help-consistency-fixes/tasks.md @@ -0,0 +1,41 @@ +# Tasks + +## Fix dead relate/work-graph dispatch + +- [x] Remove `relate`/`work-graph` from `knownSubcommands` in `main.go` +- [x] Remove `relate`/`work-graph` from the unknown-subcommand hint text +- [x] Remove the false `relate` documentation from README (subcommand list + + `### relate` section) +- [x] Verify `specsync relate` now errors instead of running a live sync + +## Fix agent-help/doctor flag-order parsing + +- [x] Add `reorderFlagsFirst` helper in `main.go` +- [x] Use it in `runAgentHelp`'s `fs.Parse` +- [x] Use it in `runDoctor`'s `fs.Parse` +- [x] Verify `agent-help -json` and `agent-help -json ` both work +- [x] Verify `doctor -json` and `doctor -json ` both work + +## Add missing agent-help entries + +- [x] `doctor` +- [x] `epic` +- [x] `idea` +- [x] `ideas` +- [x] `archive` +- [x] `set-priority` +- [x] `note` +- [x] Verify `agent-help` overview lists all 25 subcommands + +## Standardize dash style + +- [x] `WORKFLOW.md` (`sync --dry-run` → `sync -dry-run`) +- [x] `README.md` (`ideas --json` → `ideas -json`) +- [x] `CLAUDE.md` (`changes --json` → `changes -json`) +- [x] `agent_help.go` example/tip strings (`--json` → `-json`) + +## Validation + +- [x] `go build ./...` +- [x] `go vet ./...` +- [x] `go test ./...`