diff --git a/docs/inbox-cli.md b/docs/inbox-cli.md index 430bd74..a7a1087 100644 --- a/docs/inbox-cli.md +++ b/docs/inbox-cli.md @@ -113,6 +113,15 @@ Rules: The binary name is `inbox`. +Built-in help should be sufficient for first use: + +- root help should explain the worker role and the normal fetch -> claim -> update -> wait-reply -> done/fail loop +- command help should explain when to use the command, not just list flags +- high-frequency commands should include concrete examples that can be copied directly +- `list` help should explain how it differs from `fetch` +- `show` help should explain that it is the thread-history inspection command +- `watch` help should explain how it differs from `wait-reply` + ### Global Flags - `--db PATH` diff --git a/docs/orch-cli.md b/docs/orch-cli.md index 89c4cba..d96547c 100644 --- a/docs/orch-cli.md +++ b/docs/orch-cli.md @@ -110,6 +110,16 @@ The leader should block on `orch wait`, not on ad hoc `sleep`. The binary name is `orch`. +Built-in help should be sufficient for first use: + +- root help should explain the leader role and the normal run -> task -> dep -> ready -> dispatch -> wait/status loop +- command help should explain the scheduling contract, not just list flags +- `dispatch` help should explicitly explain `--execution-mode analysis|code` and which flags only apply to code mode +- high-frequency commands should include concrete examples that can be copied directly +- `status` help should explain that it is the main operational dashboard command +- `blocked` help should explain that it is the compact queue to inspect before `answer` +- `cleanup` help should explain how `--task`, `--attempt`, and `--all-completed` change cleanup scope + ### Global Flags - `--db PATH` diff --git a/packages/inbox-runtime/internal/cli/inbox/cancel.go b/packages/inbox-runtime/internal/cli/inbox/cancel.go index c980f67..3c3f303 100644 --- a/packages/inbox-runtime/internal/cli/inbox/cancel.go +++ b/packages/inbox-runtime/internal/cli/inbox/cancel.go @@ -22,6 +22,12 @@ func newCancelCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "cancel", Short: "Cancel a thread", + Long: helpLong( + "Use cancel to terminate a thread and record a control message explaining why it should stop.", + "cancel is usually a leader or operator action rather than a normal worker completion step.", + "Use a reason when you need later readers to understand why the thread was stopped.", + ), + Example: ` inbox --db .agents/coord.db cancel --agent leader --thread thr_123 --reason "Task superseded by a new plan."`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/inbox-runtime/internal/cli/inbox/claim.go b/packages/inbox-runtime/internal/cli/inbox/claim.go index 0ddb6d9..8dc4740 100644 --- a/packages/inbox-runtime/internal/cli/inbox/claim.go +++ b/packages/inbox-runtime/internal/cli/inbox/claim.go @@ -22,6 +22,14 @@ func newClaimCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "claim", Short: "Acquire a lease on a pending thread", + Long: helpLong( + "Use claim to acquire the active worker lease for one thread.", + "claim is the step that turns a candidate thread into owned work.", + "After claim, the worker should inspect the thread, send an in_progress update when real work starts, and finish with done or fail.", + "Only one active lease may exist per thread at a time.", + ), + Example: ` inbox --db .agents/coord.db claim --agent worker-a --thread thr_123 + inbox --db .agents/coord.db claim --agent worker-a --thread thr_123 --lease-seconds 1800`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/inbox-runtime/internal/cli/inbox/done.go b/packages/inbox-runtime/internal/cli/inbox/done.go index 8ba49db..3eaaafd 100644 --- a/packages/inbox-runtime/internal/cli/inbox/done.go +++ b/packages/inbox-runtime/internal/cli/inbox/done.go @@ -33,6 +33,22 @@ func newCompleteCmd(root *rootOptions, mode string) *cobra.Command { cmd := &cobra.Command{ Use: mode, Short: map[string]string{"done": "Mark a thread complete", "fail": "Mark a thread failed"}[mode], + Long: map[string]string{ + "done": helpLong( + "Use done to close a claimed thread successfully and record the final result summary.", + "done is a terminal operation; use it only when the work is complete enough to hand back to the leader.", + "Include a short summary and optionally a detailed body or artifacts for the final result.", + ), + "fail": helpLong( + "Use fail to close a claimed thread unsuccessfully and record the failure summary.", + "fail is a terminal operation; use it when the attempt cannot complete successfully.", + "Include a short summary and enough detail for the leader to decide on retry, reassignment, or cancellation.", + ), + }[mode], + Example: map[string]string{ + "done": ` inbox --db .agents/coord.db done --agent worker-a --thread thr_123 --summary "Implemented retry policy" --body "Retries now cover read timeouts."`, + "fail": ` inbox --db .agents/coord.db fail --agent worker-a --thread thr_123 --summary "Blocked by migration issue" --body "Previous schema state is inconsistent."`, + }[mode], RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/inbox-runtime/internal/cli/inbox/fetch.go b/packages/inbox-runtime/internal/cli/inbox/fetch.go index 0564621..a4c8b08 100644 --- a/packages/inbox-runtime/internal/cli/inbox/fetch.go +++ b/packages/inbox-runtime/internal/cli/inbox/fetch.go @@ -23,6 +23,14 @@ func newFetchCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "fetch", Short: "List candidate threads for an agent", + Long: helpLong( + "Use fetch to discover threads that are candidates for one agent to claim.", + "fetch does not grant ownership; it is only the discovery step before claim.", + "The normal worker loop is fetch -> claim -> show -> update/done/fail.", + "Use --unread when you only want threads whose latest message has not been consumed by that agent.", + ), + Example: ` inbox --db .agents/coord.db fetch --agent worker-a --status pending + inbox --db .agents/coord.db fetch --agent worker-a --status pending,blocked --unread --limit 5`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/inbox-runtime/internal/cli/inbox/help.go b/packages/inbox-runtime/internal/cli/inbox/help.go new file mode 100644 index 0000000..70ae7da --- /dev/null +++ b/packages/inbox-runtime/internal/cli/inbox/help.go @@ -0,0 +1,24 @@ +package inbox + +import "strings" + +func helpLong(purpose string, constraints ...string) string { + var builder strings.Builder + builder.WriteString(strings.TrimSpace(purpose)) + if len(constraints) == 0 { + return builder.String() + } + + builder.WriteString("\n\nConstraints:\n") + for _, constraint := range constraints { + constraint = strings.TrimSpace(constraint) + if constraint == "" { + continue + } + builder.WriteString("- ") + builder.WriteString(constraint) + builder.WriteByte('\n') + } + + return strings.TrimRight(builder.String(), "\n") +} diff --git a/packages/inbox-runtime/internal/cli/inbox/help_contracts_test.go b/packages/inbox-runtime/internal/cli/inbox/help_contracts_test.go new file mode 100644 index 0000000..5ca3590 --- /dev/null +++ b/packages/inbox-runtime/internal/cli/inbox/help_contracts_test.go @@ -0,0 +1,83 @@ +package inbox + +import ( + "strings" + "testing" +) + +func TestInboxRootHelpExplainsWorkerLoop(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeInboxCommand("--help") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(combined, "Workers should use inbox directly") { + t.Fatalf("expected root help to explain worker role, got:\n%s", combined) + } + if !strings.Contains(combined, "Constraints:") { + t.Fatalf("expected root help to include constraints section, got:\n%s", combined) + } + if !strings.Contains(combined, "fetch --agent worker-a --status pending") { + t.Fatalf("expected root help to include a concrete workflow example, got:\n%s", combined) + } +} + +func TestInboxUpdateHelpExplainsBlockedQuestions(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeInboxCommand("update", "--help") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(combined, "Blocked updates should include") { + t.Fatalf("expected update help to explain blocked questions, got:\n%s", combined) + } + if !strings.Contains(combined, "Constraints:") { + t.Fatalf("expected update help to include constraints section, got:\n%s", combined) + } + if !strings.Contains(combined, `--payload-json '{"question":"Use stdout or stderr?"}'`) { + t.Fatalf("expected update help to include payload-json example, got:\n%s", combined) + } +} + +func TestInboxWaitReplyHelpExplainsBlockingPrimitive(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeInboxCommand("wait-reply", "--help") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(combined, "worker-side blocking primitive") { + t.Fatalf("expected wait-reply help to explain its role, got:\n%s", combined) + } + if !strings.Contains(combined, "--after-event 42 --timeout-seconds 900") { + t.Fatalf("expected wait-reply help to include resume example, got:\n%s", combined) + } +} + +func TestInboxListHelpExplainsDifferenceFromFetch(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeInboxCommand("list", "--help") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(combined, "Compared with fetch") { + t.Fatalf("expected list help to explain how it differs from fetch, got:\n%s", combined) + } + if !strings.Contains(combined, "Constraints:") { + t.Fatalf("expected list help to include constraints section, got:\n%s", combined) + } + if !strings.Contains(combined, "--created-by orch --status done,failed") { + t.Fatalf("expected list help to include inspection-focused example, got:\n%s", combined) + } +} diff --git a/packages/inbox-runtime/internal/cli/inbox/init.go b/packages/inbox-runtime/internal/cli/inbox/init.go index 2be90d4..c36c4d5 100644 --- a/packages/inbox-runtime/internal/cli/inbox/init.go +++ b/packages/inbox-runtime/internal/cli/inbox/init.go @@ -12,6 +12,12 @@ func newInitCmd(opts *rootOptions) *cobra.Command { return &cobra.Command{ Use: "init", Short: "Initialize the shared SQLite database schema", + Long: helpLong( + "Use init to create or migrate the shared inbox SQLite schema at one database path.", + "Run init once before first real use on a new database path.", + "init is safe to rerun when you need to ensure the schema exists before another command.", + ), + Example: ` inbox --db .agents/coord.db init`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/inbox-runtime/internal/cli/inbox/list.go b/packages/inbox-runtime/internal/cli/inbox/list.go index 4c99f28..f9fb7d9 100644 --- a/packages/inbox-runtime/internal/cli/inbox/list.go +++ b/packages/inbox-runtime/internal/cli/inbox/list.go @@ -23,6 +23,14 @@ func newListCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List threads with filters", + Long: helpLong( + "Use list for broad inbox inspection across many threads.", + "Compared with fetch: fetch is the worker discovery step before claim, while list is the general inspection surface for leaders, operators, and debugging.", + "Use list when you want to scan by assigned worker, creator, or status without implying that the current agent plans to claim the work.", + ), + Example: ` inbox --db .agents/coord.db list --status blocked,in_progress + inbox --db .agents/coord.db list --assigned-to worker-a --status pending --limit 10 + inbox --db .agents/coord.db list --created-by orch --status done,failed`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -73,11 +81,11 @@ func newListCmd(root *rootOptions) *cobra.Command { }, } - cmd.Flags().StringVar(&opts.agent, "agent", "", "Assigned agent filter shortcut") - cmd.Flags().StringVar(&opts.statuses, "status", "", "Comma-separated status filter") - cmd.Flags().StringVar(&opts.createdBy, "created-by", "", "Created-by filter") - cmd.Flags().StringVar(&opts.assignedTo, "assigned-to", "", "Assigned-to filter") - cmd.Flags().IntVar(&opts.limit, "limit", 20, "Maximum number of threads") + cmd.Flags().StringVar(&opts.agent, "agent", "", "Shortcut for filtering threads assigned to one agent") + cmd.Flags().StringVar(&opts.statuses, "status", "", "Comma-separated status filter such as pending,blocked,done") + cmd.Flags().StringVar(&opts.createdBy, "created-by", "", "Only include threads created by this agent") + cmd.Flags().StringVar(&opts.assignedTo, "assigned-to", "", "Only include threads currently assigned to this agent") + cmd.Flags().IntVar(&opts.limit, "limit", 20, "Maximum number of threads to return") return cmd } diff --git a/packages/inbox-runtime/internal/cli/inbox/renew.go b/packages/inbox-runtime/internal/cli/inbox/renew.go index 735d1b0..acaefae 100644 --- a/packages/inbox-runtime/internal/cli/inbox/renew.go +++ b/packages/inbox-runtime/internal/cli/inbox/renew.go @@ -21,6 +21,13 @@ func newRenewCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "renew", Short: "Extend an existing lease", + Long: helpLong( + "Use renew to extend an active worker lease on one claimed thread.", + "renew only applies when the caller already owns the active lease.", + "Use renew for long-running work instead of risking lease expiry mid-execution.", + ), + Example: ` inbox --db .agents/coord.db renew --agent worker-a --thread thr_123 + inbox --db .agents/coord.db renew --agent worker-a --thread thr_123 --lease-seconds 1800`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/inbox-runtime/internal/cli/inbox/reply.go b/packages/inbox-runtime/internal/cli/inbox/reply.go index db1387a..92e1385 100644 --- a/packages/inbox-runtime/internal/cli/inbox/reply.go +++ b/packages/inbox-runtime/internal/cli/inbox/reply.go @@ -27,6 +27,14 @@ func newReplyCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "reply", Short: "Reply inside an existing thread", + Long: helpLong( + "Use reply to append a directed answer, control note, or follow-up message to an existing thread.", + "Leaders usually use reply or orch answer when a worker is blocked.", + "Workers may also use reply for non-terminal coordination that should stay inside the same thread history.", + "reply is for one existing thread; use send when you need to create a new thread.", + ), + Example: ` inbox --db .agents/coord.db reply --from leader --to worker-a --thread thr_123 --summary "Use stdout" --body "Use stdout for MVP." + inbox --db .agents/coord.db reply --from leader --to worker-a --thread thr_123 --kind control --summary "Proceed with option A" --payload-json '{"decision":"option_a"}'`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/inbox-runtime/internal/cli/inbox/root.go b/packages/inbox-runtime/internal/cli/inbox/root.go index 56045e6..2184f72 100644 --- a/packages/inbox-runtime/internal/cli/inbox/root.go +++ b/packages/inbox-runtime/internal/cli/inbox/root.go @@ -14,8 +14,22 @@ func NewRootCmd() *cobra.Command { opts := &rootOptions{} cmd := &cobra.Command{ - Use: "inbox", - Short: "Worker-facing durable coordination bus", + Use: "inbox", + Short: "Worker-facing durable coordination bus", + Long: helpLong( + "Use inbox to coordinate durable worker-side execution through SQLite-backed threads, messages, leases, and artifacts.", + "Workers should use inbox directly to fetch or inspect one assigned thread, claim it, report progress, ask blocked questions, wait for answers, and finish with done or fail.", + "Leaders usually use orch for planning and dispatch, then use inbox mainly for inspection or manual repair.", + "fetch does not grant ownership; claim is the step that acquires the active lease.", + ), + Example: ` inbox --db .agents/coord.db init + inbox --db .agents/coord.db fetch --agent worker-a --status pending + inbox --db .agents/coord.db claim --agent worker-a --thread thr_123 + inbox --db .agents/coord.db show --thread thr_123 + inbox --db .agents/coord.db update --agent worker-a --thread thr_123 --status in_progress --summary "Started work" + inbox --db .agents/coord.db update --agent worker-a --thread thr_123 --status blocked --summary "Need API decision" --payload-json '{"question":"Use v1 or v2?"}' + inbox --db .agents/coord.db wait-reply --thread thr_123 --after-event 10 + inbox --db .agents/coord.db done --agent worker-a --thread thr_123 --summary "Finished"`, SilenceErrors: true, SilenceUsage: true, } diff --git a/packages/inbox-runtime/internal/cli/inbox/send.go b/packages/inbox-runtime/internal/cli/inbox/send.go index cf46a03..e1b0831 100644 --- a/packages/inbox-runtime/internal/cli/inbox/send.go +++ b/packages/inbox-runtime/internal/cli/inbox/send.go @@ -31,6 +31,14 @@ func newSendCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "send", Short: "Create a thread with an initial directed message", + Long: helpLong( + "Use send to create a new directed thread and first message.", + "This is the low-level thread creation primitive.", + "Leaders often create task threads through orch dispatch instead; use inbox send for manual repair, ad hoc coordination, or a thread that does not belong to an orch-managed task.", + "Creating a new thread requires a subject unless you are explicitly targeting an existing thread ID.", + ), + Example: ` inbox --db .agents/coord.db send --from leader --to worker-a --subject "Investigate flaky test" --summary "Check latest failures" --run run_blog_001 --task T1 + inbox --db .agents/coord.db send --from leader --to worker-a --subject "Review logs" --summary "See attached stacktrace" --body-file ./brief.md --artifact ./failure.log --artifact-kind log`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/inbox-runtime/internal/cli/inbox/show.go b/packages/inbox-runtime/internal/cli/inbox/show.go index f08dba0..aa8cd39 100644 --- a/packages/inbox-runtime/internal/cli/inbox/show.go +++ b/packages/inbox-runtime/internal/cli/inbox/show.go @@ -20,6 +20,14 @@ func newShowCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "show", Short: "Show one thread with message history", + Long: helpLong( + "Use show to inspect one thread together with its message history.", + "Workers should usually inspect the assigned thread before making assumptions about task body, payload JSON, or prior coordination.", + "Leaders can also use show for manual debugging and repair.", + "Use show when you need the exact message sequence inside one thread, not just a filtered thread list.", + ), + Example: ` inbox --db .agents/coord.db show --thread thr_123 + inbox --db .agents/coord.db --agent worker-a show --thread thr_123 --mark-read`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -71,7 +79,7 @@ func newShowCmd(root *rootOptions) *cobra.Command { } cmd.Flags().StringVar(&opts.threadID, "thread", "", "Thread ID") - cmd.Flags().BoolVar(&opts.markRead, "mark-read", false, "Advance the caller's read cursor to the latest message") + cmd.Flags().BoolVar(&opts.markRead, "mark-read", false, "Advance the caller's read cursor to the latest message in this thread") _ = cmd.MarkFlagRequired("thread") return cmd diff --git a/packages/inbox-runtime/internal/cli/inbox/update.go b/packages/inbox-runtime/internal/cli/inbox/update.go index 4cdfe17..90f9a10 100644 --- a/packages/inbox-runtime/internal/cli/inbox/update.go +++ b/packages/inbox-runtime/internal/cli/inbox/update.go @@ -26,6 +26,14 @@ func newUpdateCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "update", Short: "Append a progress or blocked update to a thread", + Long: helpLong( + "Use update to append worker progress inside one claimed thread.", + "in_progress means real work has started.", + "blocked means the worker cannot continue without a precise answer or dependency.", + "Blocked updates should include a concise summary and usually a payload_json question so the leader can answer deterministically.", + ), + Example: ` inbox --db .agents/coord.db update --agent worker-a --thread thr_123 --status in_progress --summary "Reading current auth flow" + inbox --db .agents/coord.db update --agent worker-a --thread thr_123 --status blocked --summary "Need logging decision" --payload-json '{"question":"Use stdout or stderr?"}'`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/inbox-runtime/internal/cli/inbox/wait_reply.go b/packages/inbox-runtime/internal/cli/inbox/wait_reply.go index 63c3b1e..9a065ce 100644 --- a/packages/inbox-runtime/internal/cli/inbox/wait_reply.go +++ b/packages/inbox-runtime/internal/cli/inbox/wait_reply.go @@ -24,6 +24,15 @@ func newWaitReplyCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "wait-reply", Short: "Block until a reply-like message appears in a thread", + Long: helpLong( + "Use wait-reply after a worker has marked a thread blocked and needs a leader response.", + "This is the worker-side blocking primitive; prefer it over ad hoc sleep loops.", + "Resume with --after-event or --after-message when you already know the last event or message you processed.", + "wait-reply watches one thread, unlike watch which can observe broader inbox activity.", + ), + Example: ` inbox --db .agents/coord.db wait-reply --thread thr_123 + inbox --db .agents/coord.db wait-reply --thread thr_123 --after-event 42 --timeout-seconds 900 + inbox --db .agents/coord.db wait-reply --thread thr_123 --kinds answer,result`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/inbox-runtime/internal/cli/inbox/watch.go b/packages/inbox-runtime/internal/cli/inbox/watch.go index 2a128f8..5b57f0f 100644 --- a/packages/inbox-runtime/internal/cli/inbox/watch.go +++ b/packages/inbox-runtime/internal/cli/inbox/watch.go @@ -23,6 +23,13 @@ func newWatchCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "watch", Short: "Block until new matching activity appears", + Long: helpLong( + "Use watch to wait for new matching thread activity across the inbox.", + "This is useful for operator-style inspection or lightweight agents that need to observe when new work, blocked work, or terminal results appear without polling manually.", + "watch is broader than wait-reply: it watches many matching threads, while wait-reply is the worker-side primitive for one blocked thread.", + ), + Example: ` inbox --db .agents/coord.db watch --status pending,blocked + inbox --db .agents/coord.db watch --agent worker-a --status pending --after-event 100 --timeout-seconds 300`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -81,10 +88,10 @@ func newWatchCmd(root *rootOptions) *cobra.Command { }, } - cmd.Flags().StringVar(&opts.agent, "agent", "", "Assigned agent filter") - cmd.Flags().StringVar(&opts.statuses, "status", "pending,blocked,done,failed", "Comma-separated status filter") - cmd.Flags().IntVar(&opts.timeoutSeconds, "timeout-seconds", 0, "Maximum time to wait; 0 waits forever") - cmd.Flags().Int64Var(&opts.afterEventID, "after-event", 0, "Resume after a known event ID") + cmd.Flags().StringVar(&opts.agent, "agent", "", "Only wake for threads assigned to this agent") + cmd.Flags().StringVar(&opts.statuses, "status", "pending,blocked,done,failed", "Comma-separated thread status filter") + cmd.Flags().IntVar(&opts.timeoutSeconds, "timeout-seconds", 0, "Maximum time to wait; 0 means wait forever") + cmd.Flags().Int64Var(&opts.afterEventID, "after-event", 0, "Resume after a known inbox event ID") return cmd } diff --git a/packages/orch-runtime/internal/cli/orch/answer.go b/packages/orch-runtime/internal/cli/orch/answer.go index 5f80508..2117b14 100644 --- a/packages/orch-runtime/internal/cli/orch/answer.go +++ b/packages/orch-runtime/internal/cli/orch/answer.go @@ -23,6 +23,13 @@ func newAnswerCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "answer", Short: "Answer the active blocked question for a task", + Long: helpLong( + "Use answer to write one response back into the active blocked attempt thread for a task.", + "Use body for human-readable guidance, payload-json for structured decisions, or both when workers need a stable machine-readable contract.", + "answer targets the current active blocked attempt for the task; it is not a generic freeform message append.", + ), + Example: ` orch --db .agents/coord.db answer --run blog_mvp_001 --task T2 --body "Use stdout for MVP." + orch --db .agents/coord.db answer --run blog_mvp_001 --task T2 --payload-json '{"decision":"stdout","source":"leader"}'`, RunE: func(cmd *cobra.Command, args []string) error { body, err := resolveBodyValue(opts.body, opts.bodyFile) if err != nil { diff --git a/packages/orch-runtime/internal/cli/orch/blocked.go b/packages/orch-runtime/internal/cli/orch/blocked.go index dd59880..31fb67c 100644 --- a/packages/orch-runtime/internal/cli/orch/blocked.go +++ b/packages/orch-runtime/internal/cli/orch/blocked.go @@ -19,6 +19,12 @@ func newBlockedCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "blocked", Short: "List blocked tasks and their latest question", + Long: helpLong( + "Use blocked to list tasks whose active attempt is blocked together with the latest question the leader needs to answer.", + "Use blocked before answer when you want a compact queue of unresolved worker questions instead of the full run view from status.", + ), + Example: ` orch --db .agents/coord.db blocked --run blog_mvp_001 + orch --db .agents/coord.db --json blocked --run blog_mvp_001`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/cancel.go b/packages/orch-runtime/internal/cli/orch/cancel.go index 74ce970..35085c4 100644 --- a/packages/orch-runtime/internal/cli/orch/cancel.go +++ b/packages/orch-runtime/internal/cli/orch/cancel.go @@ -21,6 +21,13 @@ func newCancelCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "cancel", Short: "Cancel a task or an entire run", + Long: helpLong( + "Use cancel to stop one task or an entire run.", + "Pass --task when you want to cancel one task inside the run; omit it when you want to cancel the whole run.", + "Use a reason when later readers need to understand why the work was stopped.", + ), + Example: ` orch --db .agents/coord.db cancel --run blog_mvp_001 --task T3 --reason "Superseded by a new plan." + orch --db .agents/coord.db cancel --run blog_mvp_001 --reason "User cancelled the entire request."`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/cleanup.go b/packages/orch-runtime/internal/cli/orch/cleanup.go index d652041..5c0b924 100644 --- a/packages/orch-runtime/internal/cli/orch/cleanup.go +++ b/packages/orch-runtime/internal/cli/orch/cleanup.go @@ -23,6 +23,16 @@ func newCleanupCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "cleanup", Short: "Remove completed or abandoned attempt worktrees", + Long: helpLong( + "Use cleanup to remove worktrees that belong to completed, abandoned, or explicitly forced attempt cleanup.", + "cleanup only affects attempts that actually have worktree-backed execution state; analysis-mode attempts have no worktree to remove.", + "Pass --task when you want to clean one task's latest cleanup candidate.", + "Pass --task plus --attempt when you want one exact attempt.", + "Pass --all-completed when you want a run-wide cleanup sweep.", + ), + Example: ` orch --db .agents/coord.db cleanup --run blog_mvp_001 --task T1 + orch --db .agents/coord.db cleanup --run blog_mvp_001 --task T1 --attempt 2 + orch --db .agents/coord.db cleanup --run blog_mvp_001 --all-completed`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -74,9 +84,9 @@ func newCleanupCmd(root *rootOptions) *cobra.Command { } cmd.Flags().StringVar(&opts.runID, "run", "", "Run ID") - cmd.Flags().StringVar(&opts.taskID, "task", "", "Optional task ID") - cmd.Flags().IntVar(&opts.attemptNo, "attempt", 0, "Specific attempt number") - cmd.Flags().BoolVar(&opts.allCompleted, "all-completed", false, "Clean all completed or abandoned worktrees in the run") + cmd.Flags().StringVar(&opts.taskID, "task", "", "Limit cleanup candidates to one task") + cmd.Flags().IntVar(&opts.attemptNo, "attempt", 0, "Limit cleanup to one specific attempt number") + cmd.Flags().BoolVar(&opts.allCompleted, "all-completed", false, "Clean every completed or abandoned worktree in the run") cmd.Flags().BoolVar(&opts.force, "force", false, "Force cleanup even for non-terminal worktrees") _ = cmd.MarkFlagRequired("run") diff --git a/packages/orch-runtime/internal/cli/orch/council.go b/packages/orch-runtime/internal/cli/orch/council.go index 05fde1d..5f837e3 100644 --- a/packages/orch-runtime/internal/cli/orch/council.go +++ b/packages/orch-runtime/internal/cli/orch/council.go @@ -6,6 +6,14 @@ func newCouncilCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "council", Short: "Council review workflow commands", + Long: helpLong( + "Use council commands for the three-reviewer analysis workflow built on top of orch runs, tasks, and inbox threads.", + "council is analysis-oriented and is meant for grouped review output rather than direct code-writing execution.", + ), + Example: ` orch --db .agents/coord.db council start --run council_blog_001 --target "Review the current architecture." + orch --db .agents/coord.db council wait --run council_blog_001 --timeout-seconds 600 + orch --db .agents/coord.db council tally --run council_blog_001 + orch --db .agents/coord.db council report --run council_blog_001`, } cmd.AddCommand(newCouncilStartCmd(root)) diff --git a/packages/orch-runtime/internal/cli/orch/council_report.go b/packages/orch-runtime/internal/cli/orch/council_report.go index ff1a0bf..34fcc5b 100644 --- a/packages/orch-runtime/internal/cli/orch/council_report.go +++ b/packages/orch-runtime/internal/cli/orch/council_report.go @@ -23,6 +23,12 @@ func newCouncilReportCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "report", Short: "Render the final grouped council output", + Long: helpLong( + "Use council report to render the final council output from previously tallied grouped recommendations and persist the markdown artifact on disk.", + "Run report after tally so the grouped recommendations and vote buckets are already available.", + ), + Example: ` orch --db .agents/coord.db council report --run council_blog_001 + orch --db .agents/coord.db council report --run council_blog_001 --show consensus,majority`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/council_start.go b/packages/orch-runtime/internal/cli/orch/council_start.go index 105d5e4..7105850 100644 --- a/packages/orch-runtime/internal/cli/orch/council_start.go +++ b/packages/orch-runtime/internal/cli/orch/council_start.go @@ -27,6 +27,12 @@ func newCouncilStartCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "start", Short: "Create and dispatch a three-reviewer council run", + Long: helpLong( + "Use council start to create one three-reviewer council run and immediately dispatch the fixed reviewer tasks.", + "This is analysis-oriented workflow setup on top of orch.", + "The result is a normal run with reviewer tasks that later feed council wait, tally, and report.", + ), + Example: ` orch --db .agents/coord.db council start --run council_blog_001 --target "Review the current blog architecture." --target-type mixed --output both`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/council_tally.go b/packages/orch-runtime/internal/cli/orch/council_tally.go index a246003..901b796 100644 --- a/packages/orch-runtime/internal/cli/orch/council_tally.go +++ b/packages/orch-runtime/internal/cli/orch/council_tally.go @@ -20,6 +20,11 @@ func newCouncilTallyCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "tally", Short: "Group reviewer findings and compute council support counts", + Long: helpLong( + "Use council tally to read reviewer outputs, group similar proposals, and compute support counts such as consensus, majority, and minority.", + "Run tally after reviewer completion and before rendering the final report.", + ), + Example: ` orch --db .agents/coord.db council tally --run council_blog_001 --similarity normal`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/council_wait.go b/packages/orch-runtime/internal/cli/orch/council_wait.go index ce4417f..cc38833 100644 --- a/packages/orch-runtime/internal/cli/orch/council_wait.go +++ b/packages/orch-runtime/internal/cli/orch/council_wait.go @@ -21,6 +21,11 @@ func newCouncilWaitCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "wait", Short: "Block until all council reviewers complete or timeout is reached", + Long: helpLong( + "Use council wait to block until all reviewer tasks in one council run have completed or the timeout is reached.", + "Use this after council start instead of polling reviewer task state manually.", + ), + Example: ` orch --db .agents/coord.db council wait --run council_blog_001 --timeout-seconds 900`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/dep.go b/packages/orch-runtime/internal/cli/orch/dep.go index 5f2e7f1..5a8f333 100644 --- a/packages/orch-runtime/internal/cli/orch/dep.go +++ b/packages/orch-runtime/internal/cli/orch/dep.go @@ -10,15 +10,20 @@ import ( ) type depAddOptions struct { - runID string - taskID string - dependsOn string + runID string + taskID string + dependsOn string } func newDepCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "dep", Short: "Task dependency commands", + Long: helpLong( + "Use dep commands to gate one task on another task's completion.", + "Dependencies affect the ready queue; they do not themselves launch or cancel work.", + ), + Example: ` orch --db .agents/coord.db dep add --run blog_mvp_001 --task T2 --depends-on T1`, } cmd.AddCommand(newDepAddCmd(root)) @@ -31,6 +36,11 @@ func newDepAddCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "add", Short: "Add a dependency edge to a task", + Long: helpLong( + "Use dep add to make one task wait for another task to finish before it becomes ready.", + "The dependency target and dependent task must already exist in the same run.", + ), + Example: ` orch --db .agents/coord.db dep add --run blog_mvp_001 --task frontend --depends-on backend`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/dispatch.go b/packages/orch-runtime/internal/cli/orch/dispatch.go index ba88a12..ac7daf9 100644 --- a/packages/orch-runtime/internal/cli/orch/dispatch.go +++ b/packages/orch-runtime/internal/cli/orch/dispatch.go @@ -27,6 +27,16 @@ func newDispatchCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "dispatch", Short: "Dispatch a ready task to a worker through inbox", + Long: helpLong( + "Use dispatch to turn one ready task into a concrete attempt and inbox thread.", + "You must choose exactly one execution mode.", + "analysis: create the attempt and thread only; do not allocate a worktree.", + "code: allocate a Git worktree for the attempt and record workspace metadata.", + "repo-path, workspace-root, and base-ref only apply to execution-mode code.", + "dispatch creates handoff state only; a separate worker runtime or worker agent must still claim the assigned inbox thread.", + ), + Example: ` orch --db .agents/coord.db dispatch --run blog_mvp_001 --task T1 --execution-mode analysis --to qa-worker --body "Summarize the latest failures." + orch --db .agents/coord.db dispatch --run blog_mvp_001 --task T2 --execution-mode code --to backend-worker --repo-path /path/to/repo --workspace-root .orch/worktrees --base-ref main --body-file ./tasks/t2.md`, RunE: func(cmd *cobra.Command, args []string) error { normalizedOpts, err := normalizeDispatchOptions(*opts) if err != nil { diff --git a/packages/orch-runtime/internal/cli/orch/help.go b/packages/orch-runtime/internal/cli/orch/help.go new file mode 100644 index 0000000..e8e99b9 --- /dev/null +++ b/packages/orch-runtime/internal/cli/orch/help.go @@ -0,0 +1,24 @@ +package orch + +import "strings" + +func helpLong(purpose string, constraints ...string) string { + var builder strings.Builder + builder.WriteString(strings.TrimSpace(purpose)) + if len(constraints) == 0 { + return builder.String() + } + + builder.WriteString("\n\nConstraints:\n") + for _, constraint := range constraints { + constraint = strings.TrimSpace(constraint) + if constraint == "" { + continue + } + builder.WriteString("- ") + builder.WriteString(constraint) + builder.WriteByte('\n') + } + + return strings.TrimRight(builder.String(), "\n") +} diff --git a/packages/orch-runtime/internal/cli/orch/help_contracts_test.go b/packages/orch-runtime/internal/cli/orch/help_contracts_test.go new file mode 100644 index 0000000..4c966de --- /dev/null +++ b/packages/orch-runtime/internal/cli/orch/help_contracts_test.go @@ -0,0 +1,100 @@ +package orch + +import ( + "strings" + "testing" +) + +func TestOrchRootHelpExplainsLeaderWorkflow(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeOrchCommand("--help") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(combined, "orch is the control plane") { + t.Fatalf("expected root help to explain control-plane role, got:\n%s", combined) + } + if !strings.Contains(combined, "Constraints:") { + t.Fatalf("expected root help to include constraints section, got:\n%s", combined) + } + if !strings.Contains(combined, "dispatch --run blog_mvp_001 --task T1 --execution-mode analysis") { + t.Fatalf("expected root help to include execution-mode example, got:\n%s", combined) + } +} + +func TestOrchDispatchHelpExplainsExecutionModes(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeOrchCommand("dispatch", "--help") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(combined, "analysis: create the attempt and thread only") { + t.Fatalf("expected dispatch help to explain analysis mode, got:\n%s", combined) + } + if !strings.Contains(combined, "Constraints:") { + t.Fatalf("expected dispatch help to include constraints section, got:\n%s", combined) + } + if !strings.Contains(combined, "repo-path, workspace-root, and base-ref only apply to execution-mode code") { + t.Fatalf("expected dispatch help to explain code-only flags, got:\n%s", combined) + } +} + +func TestOrchCouncilStartHelpExplainsWorkflow(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeOrchCommand("council", "start", "--help") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(combined, "three-reviewer council run") { + t.Fatalf("expected council start help to explain purpose, got:\n%s", combined) + } + if !strings.Contains(combined, `--target "Review the current blog architecture."`) { + t.Fatalf("expected council start help to include an example, got:\n%s", combined) + } +} + +func TestOrchStatusHelpExplainsDashboardRole(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeOrchCommand("status", "--help") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(combined, "main leader dashboard command") { + t.Fatalf("expected status help to explain dashboard role, got:\n%s", combined) + } + if !strings.Contains(combined, "Constraints:") { + t.Fatalf("expected status help to include constraints section, got:\n%s", combined) + } +} + +func TestOrchCleanupHelpExplainsScopeFlags(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeOrchCommand("cleanup", "--help") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(strings.ToLower(combined), "pass --task plus --attempt when you want one exact attempt") { + t.Fatalf("expected cleanup help to explain scope flags, got:\n%s", combined) + } + if !strings.Contains(combined, "Constraints:") { + t.Fatalf("expected cleanup help to include constraints section, got:\n%s", combined) + } + if !strings.Contains(combined, "--task T1 --attempt 2") { + t.Fatalf("expected cleanup help to include exact-attempt example, got:\n%s", combined) + } +} diff --git a/packages/orch-runtime/internal/cli/orch/ready.go b/packages/orch-runtime/internal/cli/orch/ready.go index 99645de..9871728 100644 --- a/packages/orch-runtime/internal/cli/orch/ready.go +++ b/packages/orch-runtime/internal/cli/orch/ready.go @@ -20,6 +20,12 @@ func newReadyCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "ready", Short: "List tasks that are ready for dispatch", + Long: helpLong( + "Use ready to list tasks whose dependencies are already satisfied and can be dispatched now.", + "ready is a queue-inspection command only; it does not create attempts or threads.", + ), + Example: ` orch --db .agents/coord.db ready --run blog_mvp_001 + orch --db .agents/coord.db ready --run blog_mvp_001 --limit 5`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/reassign.go b/packages/orch-runtime/internal/cli/orch/reassign.go index f872100..880b6ff 100644 --- a/packages/orch-runtime/internal/cli/orch/reassign.go +++ b/packages/orch-runtime/internal/cli/orch/reassign.go @@ -22,6 +22,12 @@ func newReassignCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "reassign", Short: "Reassign a blocked or failed task to another worker", + Long: helpLong( + "Use reassign to cancel the current active attempt and create a new attempt for another worker.", + "Like retry, reassign preserves the prior execution contract: analysis attempts stay analysis-only, and code attempts receive a fresh worktree.", + "Use reassign when the worker target should change, not just when the same worker should try again.", + ), + Example: ` orch --db .agents/coord.db reassign --run blog_mvp_001 --task T3 --to worker-b --reason "Worker-a is blocked on a missing dependency."`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/reconcile.go b/packages/orch-runtime/internal/cli/orch/reconcile.go index d1e218c..1bc5b44 100644 --- a/packages/orch-runtime/internal/cli/orch/reconcile.go +++ b/packages/orch-runtime/internal/cli/orch/reconcile.go @@ -19,6 +19,12 @@ func newReconcileCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "reconcile", Short: "Reconcile inbox thread state back into orch task state", + Long: helpLong( + "Use reconcile to fold inbox thread state back into orch task and run state.", + "reconcile is the bridge from worker-side inbox activity to leader-side scheduler state.", + "Call reconcile before making new dispatch, retry, or status decisions when you need fresh state.", + ), + Example: ` orch --db .agents/coord.db reconcile --run blog_mvp_001`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/retry.go b/packages/orch-runtime/internal/cli/orch/retry.go index 704294c..69e3aed 100644 --- a/packages/orch-runtime/internal/cli/orch/retry.go +++ b/packages/orch-runtime/internal/cli/orch/retry.go @@ -23,6 +23,12 @@ func newRetryCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "retry", Short: "Retry a failed task by creating a new attempt", + Long: helpLong( + "Use retry to create a fresh attempt and inbox thread for one failed task.", + "If the previous attempt used a worktree, retry provisions a new worktree from the recorded workspace contract.", + "If the previous attempt was analysis-only, retry stays analysis-only.", + ), + Example: ` orch --db .agents/coord.db retry --run blog_mvp_001 --task T7 --to backend-worker --body "Retry after fixing the contract mismatch."`, RunE: func(cmd *cobra.Command, args []string) error { body, err := resolveBodyValue(opts.body, opts.bodyFile) if err != nil { diff --git a/packages/orch-runtime/internal/cli/orch/root.go b/packages/orch-runtime/internal/cli/orch/root.go index 912c5a3..59c866f 100644 --- a/packages/orch-runtime/internal/cli/orch/root.go +++ b/packages/orch-runtime/internal/cli/orch/root.go @@ -13,8 +13,19 @@ func NewRootCmd() *cobra.Command { opts := &rootOptions{} cmd := &cobra.Command{ - Use: "orch", - Short: "Leader-facing scheduler and control plane", + Use: "orch", + Short: "Leader-facing scheduler and control plane", + Long: helpLong( + "Use orch to manage leader-side scheduling for runs, tasks, dependencies, dispatch, retries, reassignment, blocked-task answers, and worktree-backed code attempts.", + "orch is the control plane; it creates durable handoff state in inbox but does not launch workers by itself.", + "After dispatch, a separate worker runtime or worker agent should claim the assigned inbox thread.", + "Use execution-mode analysis for thread-only work and execution-mode code for worktree-backed repository changes.", + ), + Example: ` orch --db .agents/coord.db run init --run blog_mvp_001 --goal "Build blog MVP" + orch --db .agents/coord.db task add --run blog_mvp_001 --task T1 --title "Summarize flaky tests" --default-to qa-worker + orch --db .agents/coord.db ready --run blog_mvp_001 + orch --db .agents/coord.db dispatch --run blog_mvp_001 --task T1 --execution-mode analysis --to qa-worker --body "Read the latest failures." + orch --db .agents/coord.db status --run blog_mvp_001`, SilenceErrors: true, SilenceUsage: true, } diff --git a/packages/orch-runtime/internal/cli/orch/run.go b/packages/orch-runtime/internal/cli/orch/run.go index 8a4b226..640a321 100644 --- a/packages/orch-runtime/internal/cli/orch/run.go +++ b/packages/orch-runtime/internal/cli/orch/run.go @@ -23,6 +23,12 @@ func newRunCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "run", Short: "Run management commands", + Long: helpLong( + "Use run commands to create or inspect one orchestration run.", + "A run is the durable container for tasks, dependencies, attempts, and events for one user request or project slice.", + ), + Example: ` orch --db .agents/coord.db run init --run blog_mvp_001 --goal "Build blog MVP" + orch --db .agents/coord.db run show --run blog_mvp_001`, } cmd.AddCommand(newRunInitCmd(root)) @@ -37,6 +43,12 @@ func newRunInitCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "init", Short: "Create a new orchestration run", + Long: helpLong( + "Use run init to create one durable run that will own tasks, dependencies, attempts, and events for a single user request or project slice.", + "run IDs should be stable within the database path and are later reused by every other orch command.", + "Create the run before adding tasks, dependencies, or dispatching work.", + ), + Example: ` orch --db .agents/coord.db run init --run blog_mvp_001 --goal "Build blog MVP" --summary "Public blog plus admin CRUD"`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -86,6 +98,11 @@ func newRunShowCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "show", Short: "Show run metadata and aggregate state", + Long: helpLong( + "Use run show to inspect the lightweight run view with run metadata and aggregate task counts.", + "Prefer status when you need the full task list and latest attempt/message context.", + ), + Example: ` orch --db .agents/coord.db run show --run blog_mvp_001`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/status.go b/packages/orch-runtime/internal/cli/orch/status.go index e655bb6..9fd370c 100644 --- a/packages/orch-runtime/internal/cli/orch/status.go +++ b/packages/orch-runtime/internal/cli/orch/status.go @@ -19,6 +19,13 @@ func newStatusCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "status", Short: "Show task state summary for the run", + Long: helpLong( + "Use status to show the full operational view for one run.", + "status reconciles inbox state first, then returns the run summary, aggregate task counts, the task list, and latest attempt/message context.", + "Use status as the main leader dashboard command. Prefer it over run show when you need the latest task-level execution picture before making the next dispatch, answer, retry, or cleanup decision.", + ), + Example: ` orch --db .agents/coord.db status --run blog_mvp_001 + orch --db .agents/coord.db --json status --run blog_mvp_001`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/task.go b/packages/orch-runtime/internal/cli/orch/task.go index 76187f5..a15a7d9 100644 --- a/packages/orch-runtime/internal/cli/orch/task.go +++ b/packages/orch-runtime/internal/cli/orch/task.go @@ -23,6 +23,11 @@ func newTaskCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "task", Short: "Task management commands", + Long: helpLong( + "Use task commands to define schedulable work inside one run.", + "Tasks should be small enough to inspect, dispatch, retry, and reconcile independently.", + ), + Example: ` orch --db .agents/coord.db task add --run blog_mvp_001 --task T1 --title "Implement backend" --default-to backend-worker`, } cmd.AddCommand(newTaskAddCmd(root)) @@ -35,6 +40,12 @@ func newTaskAddCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "add", Short: "Add a task to a run", + Long: helpLong( + "Use task add to register one schedulable task inside a run.", + "Tasks may include a default worker target, priority, and optional acceptance JSON that downstream tooling can inspect.", + "A task must belong to an existing run before it can become ready or be dispatched.", + ), + Example: ` orch --db .agents/coord.db task add --run blog_mvp_001 --task T1 --title "Implement backend" --summary "Ship the first API slice" --default-to backend-worker --priority high`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/orch-runtime/internal/cli/orch/wait.go b/packages/orch-runtime/internal/cli/orch/wait.go index 9041d6e..e77287a 100644 --- a/packages/orch-runtime/internal/cli/orch/wait.go +++ b/packages/orch-runtime/internal/cli/orch/wait.go @@ -24,6 +24,12 @@ func newWaitCmd(root *rootOptions) *cobra.Command { cmd := &cobra.Command{ Use: "wait", Short: "Block until matching run-scoped task events become available", + Long: helpLong( + "Use wait as the leader-side blocking primitive.", + "Instead of polling with manual sleep loops, wait blocks until later matching task events exist for the run, such as ready, blocked, done, or failed.", + "Use --after-event when resuming from a known cursor so you do not reprocess earlier events.", + ), + Example: ` orch --db .agents/coord.db wait --run blog_mvp_001 --for task_blocked,task_done --after-event 0 --timeout-seconds 900`, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() diff --git a/packages/repo-memory-runtime/cmd/repo-memory/execute.go b/packages/repo-memory-runtime/cmd/repo-memory/execute.go index 40f4deb..75f0874 100644 --- a/packages/repo-memory-runtime/cmd/repo-memory/execute.go +++ b/packages/repo-memory-runtime/cmd/repo-memory/execute.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "os" + "strings" "sync" ) @@ -30,6 +31,21 @@ func Execute(args []string, stdout, stderr io.Writer) int { usage() return 2 } + if args[0] == "help" { + if len(args) == 1 { + usage() + return 0 + } + if err := runCommand([]string{args[1], "--help"}); err != nil { + _, _ = fmt.Fprintln(commandStderr, err) + return 1 + } + return 0 + } + if isHelpToken(args[0]) { + usage() + return 0 + } if err := runCommand(args); err != nil { _, _ = fmt.Fprintln(commandStderr, err) @@ -39,6 +55,15 @@ func Execute(args []string, stdout, stderr io.Writer) int { return 0 } +func isHelpToken(value string) bool { + switch strings.TrimSpace(value) { + case "-h", "--help": + return true + default: + return false + } +} + func runCommand(args []string) error { switch args[0] { case "init": diff --git a/packages/repo-memory-runtime/cmd/repo-memory/help_test.go b/packages/repo-memory-runtime/cmd/repo-memory/help_test.go new file mode 100644 index 0000000..35915db --- /dev/null +++ b/packages/repo-memory-runtime/cmd/repo-memory/help_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "strings" + "testing" +) + +func TestRepoMemoryRootHelpShowsWorkflowAndCommands(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeRepoMemoryCommand("--help") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(combined, "Store durable repository knowledge in SQLite") { + t.Fatalf("expected root help to explain purpose, got:\n%s", combined) + } + if !strings.Contains(combined, "Constraints:") { + t.Fatalf("expected root help to include constraints section, got:\n%s", combined) + } + if !strings.Contains(combined, "repo-memory verify --db ~/.codex/data/repo-memory.db --repo /path/to/repo") { + t.Fatalf("expected root help to include workflow example, got:\n%s", combined) + } +} + +func TestRepoMemoryCommandHelpWorksThroughHelpSubcommand(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeRepoMemoryCommand("help", "add") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(combined, "Insert or update one durable knowledge entry") { + t.Fatalf("expected add help summary, got:\n%s", combined) + } + if !strings.Contains(combined, "Constraints:") { + t.Fatalf("expected add help to include constraints section, got:\n%s", combined) + } + if !strings.Contains(combined, "--kind") || !strings.Contains(combined, "--dep") { + t.Fatalf("expected add help to print flags, got:\n%s", combined) + } +} + +func TestRepoMemoryCommandHelpWorksWithDashHelp(t *testing.T) { + t.Parallel() + + stdout, stderr, exitCode := executeRepoMemoryCommand("search", "--help") + if exitCode != 0 { + t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout) + } + + combined := stdout + stderr + if !strings.Contains(combined, "Search stored repository knowledge before a deeper code dive") { + t.Fatalf("expected search help summary, got:\n%s", combined) + } + if !strings.Contains(combined, `--query "actionCode fill"`) { + t.Fatalf("expected search help example, got:\n%s", combined) + } +} diff --git a/packages/repo-memory-runtime/cmd/repo-memory/main.go b/packages/repo-memory-runtime/cmd/repo-memory/main.go index aa1a5d8..70eb451 100644 --- a/packages/repo-memory-runtime/cmd/repo-memory/main.go +++ b/packages/repo-memory-runtime/cmd/repo-memory/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "flag" "fmt" "os" @@ -32,8 +33,19 @@ func (s *stringSliceFlag) Set(value string) error { func runInit(args []string) error { fs := flag.NewFlagSet("init", flag.ContinueOnError) fs.SetOutput(commandStderr) + setCommandUsage(fs, "init", + "Create or migrate the SQLite schema for one repo-memory database.", + []string{ + "Run init once before first real use on a new database path.", + "init is safe to rerun when you need to ensure the schema exists before another command.", + }, + `repo-memory init --db ~/.codex/data/repo-memory.db`, + ) dbPath := fs.String("db", "repo-memory.db", "SQLite database path") if err := fs.Parse(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } return err } @@ -54,10 +66,21 @@ func runInit(args []string) error { func runIngest(args []string) error { fs := flag.NewFlagSet("ingest", flag.ContinueOnError) fs.SetOutput(commandStderr) + setCommandUsage(fs, "ingest", + "Scan markdown knowledge under one repository and import it into repo-memory.", + []string{ + "ingest expects a repository root and scans markdown under the configured relative path.", + "Use ingest for curated docs; use add when you want to record one specific fact manually.", + }, + `repo-memory ingest --db ~/.codex/data/repo-memory.db --repo /path/to/repo --path docs/ai`, + ) dbPath := fs.String("db", "repo-memory.db", "SQLite database path") repoPath := fs.String("repo", "", "Repository root") scanPath := fs.String("path", "docs/ai", "Relative path under repo to scan for markdown") if err := fs.Parse(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } return err } if strings.TrimSpace(*repoPath) == "" { @@ -120,6 +143,14 @@ func runIngest(args []string) error { func runAdd(args []string) error { fs := flag.NewFlagSet("add", flag.ContinueOnError) fs.SetOutput(commandStderr) + setCommandUsage(fs, "add", + "Insert or update one durable knowledge entry with evidence, aliases, and dependencies.", + []string{ + "add requires a repository root because entries are anchored to one repo.", + "Prefer confirmed only for well-supported facts; use dependencies and evidence fields whenever possible.", + }, + `repo-memory add --db ~/.codex/data/repo-memory.db --repo /path/to/repo --kind term --key AITask --summary "Plan task model" --source-path app/AITask.java --source-line 42 --status confirmed --alias "AI Task" --dep file:app/AITask.java:hard`, + ) dbPath := fs.String("db", "repo-memory.db", "SQLite database path") repoPath := fs.String("repo", "", "Repository root") kind := fs.String("kind", "", "Knowledge kind, e.g. term|chain|danger") @@ -139,6 +170,9 @@ func runAdd(args []string) error { fs.Var(&aliases, "alias", "Alias for this entry (repeatable)") fs.Var(&deps, "dep", "Dependency in type:locator[:hard|soft] format (repeatable)") if err := fs.Parse(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } return err } if strings.TrimSpace(*repoPath) == "" { @@ -202,11 +236,22 @@ func runAdd(args []string) error { func runSearch(args []string) error { fs := flag.NewFlagSet("search", flag.ContinueOnError) fs.SetOutput(commandStderr) + setCommandUsage(fs, "search", + "Search stored repository knowledge before a deeper code dive.", + []string{ + "search requires a non-empty query string.", + "Use repo filtering when you want to narrow results to one repository name or path fragment.", + }, + `repo-memory search --db ~/.codex/data/repo-memory.db --repo zeus --query "actionCode fill" --limit 10`, + ) dbPath := fs.String("db", "repo-memory.db", "SQLite database path") query := fs.String("query", "", "Search query") repo := fs.String("repo", "", "Optional repo path filter (substring match)") limit := fs.Int("limit", 10, "Result limit") if err := fs.Parse(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } return err } if strings.TrimSpace(*query) == "" { @@ -243,8 +288,18 @@ func runSearch(args []string) error { func runRepos(args []string) error { fs := flag.NewFlagSet("repos", flag.ContinueOnError) fs.SetOutput(commandStderr) + setCommandUsage(fs, "repos", + "List repositories currently tracked in one repo-memory database.", + []string{ + "repos reads only from the memory database; it does not rescan the filesystem.", + }, + `repo-memory repos --db ~/.codex/data/repo-memory.db`, + ) dbPath := fs.String("db", "repo-memory.db", "SQLite database path") if err := fs.Parse(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } return err } @@ -272,12 +327,23 @@ func runRepos(args []string) error { func runList(args []string) error { fs := flag.NewFlagSet("list", flag.ContinueOnError) fs.SetOutput(commandStderr) + setCommandUsage(fs, "list", + "List entries with optional repo, kind, and status filters.", + []string{ + "list is for broad inspection; use search when you need ranked text matching.", + "Filters are optional and can be combined to narrow the result set.", + }, + `repo-memory list --db ~/.codex/data/repo-memory.db --repo zeus --kind term --status confirmed --limit 20`, + ) dbPath := fs.String("db", "repo-memory.db", "SQLite database path") repo := fs.String("repo", "", "Optional repo path filter (substring match)") kind := fs.String("kind", "", "Optional knowledge kind filter") status := fs.String("status", "", "Optional status filter") limit := fs.Int("limit", 20, "Result limit") if err := fs.Parse(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } return err } @@ -311,6 +377,14 @@ func runList(args []string) error { func runEvents(args []string) error { fs := flag.NewFlagSet("events", flag.ContinueOnError) fs.SetOutput(commandStderr) + setCommandUsage(fs, "events", + "Show verification and status-change history for one entry.", + []string{ + "Resolve the entry either by --id or by the combination of --repo, --kind, and --key.", + "events is history-only; it does not modify entries.", + }, + `repo-memory events --db ~/.codex/data/repo-memory.db --id 1`, + ) dbPath := fs.String("db", "repo-memory.db", "SQLite database path") id := fs.Int64("id", 0, "Entry id") repo := fs.String("repo", "", "Repo root when resolving by kind/key") @@ -318,6 +392,9 @@ func runEvents(args []string) error { key := fs.String("key", "", "Knowledge key when resolving by kind/key") limit := fs.Int("limit", 20, "Result limit") if err := fs.Parse(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } return err } @@ -361,11 +438,22 @@ func runEvents(args []string) error { func runLink(args []string) error { fs := flag.NewFlagSet("link", flag.ContinueOnError) fs.SetOutput(commandStderr) + setCommandUsage(fs, "link", + "Create a relationship edge between two stored entries.", + []string{ + "link expects two existing entry IDs.", + "Use links for durable relationships such as related_to, depends_on, or implements.", + }, + `repo-memory link --db ~/.codex/data/repo-memory.db --from-id 1 --to-id 2 --relation related_to`, + ) dbPath := fs.String("db", "repo-memory.db", "SQLite database path") fromID := fs.Int64("from-id", 0, "From entry id") toID := fs.Int64("to-id", 0, "To entry id") relation := fs.String("relation", "", "Link relation") if err := fs.Parse(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } return err } @@ -386,9 +474,20 @@ func runLink(args []string) error { func runVerify(args []string) error { fs := flag.NewFlagSet("verify", flag.ContinueOnError) fs.SetOutput(commandStderr) + setCommandUsage(fs, "verify", + "Re-check stored entries against current repository state and downgrade stale knowledge.", + []string{ + "verify uses current git state to detect changed or missing hard dependencies.", + "Pass --repo to verify one repository; omit it to verify every tracked repository in the database.", + }, + `repo-memory verify --db ~/.codex/data/repo-memory.db --repo /path/to/repo`, + ) dbPath := fs.String("db", "repo-memory.db", "SQLite database path") repo := fs.String("repo", "", "Optional repo root to verify; if omitted, verify all known repos") if err := fs.Parse(args); err != nil { + if errors.Is(err, flag.ErrHelp) { + return nil + } return err } @@ -468,18 +567,64 @@ func usage() { _, _ = fmt.Fprintf(commandStderr, `repo-memory: repo memory CLI Usage: - repo-memory init --db repo-memory.db - repo-memory add --db repo-memory.db --repo /path/to/repo --kind term --key AITask --summary "..." - repo-memory ingest --db repo-memory.db --repo /path/to/repo [--path docs/ai] - repo-memory search --db repo-memory.db --query "actionCode fill" [--repo zeus] - repo-memory list --db repo-memory.db [--repo zeus] [--kind term] [--status confirmed] - repo-memory events --db repo-memory.db --id 1 - repo-memory link --db repo-memory.db --from-id 1 --to-id 2 --relation related_to - repo-memory verify --db repo-memory.db [--repo /path/to/repo] - repo-memory repos --db repo-memory.db + repo-memory [flags] + +Purpose: + Store durable repository knowledge in SQLite so agents can search prior findings, + ingest curated docs, add confirmed facts, inspect history, and verify stale entries. + +Constraints: + - Write durable repository knowledge, not short-lived chat conclusions. + - Prefer search before add so repeated work starts from existing knowledge. + - Use verify when code has moved enough that stored entries may be stale. + +Commands: + init Initialize or migrate one SQLite database + ingest Import markdown knowledge from docs under one repo + add Insert or update one durable knowledge entry + search Search stored knowledge by query text + list List entries with optional filters + events Show history for one entry + link Link two entries together + verify Re-check entries against current repo state + repos List tracked repositories + +Examples: + repo-memory init --db ~/.codex/data/repo-memory.db + repo-memory search --db ~/.codex/data/repo-memory.db --repo zeus --query "router auth" + repo-memory add --db ~/.codex/data/repo-memory.db --repo /path/to/repo --kind term --key AuthRouter --summary "..." + repo-memory verify --db ~/.codex/data/repo-memory.db --repo /path/to/repo + +More help: + repo-memory --help + repo-memory help add + repo-memory add --help `) } +func setCommandUsage(fs *flag.FlagSet, name, summary string, constraints []string, example string) { + fs.Usage = func() { + _, _ = fmt.Fprintf(commandStderr, "repo-memory %s\n\n", name) + _, _ = fmt.Fprintf(commandStderr, "Usage:\n repo-memory %s [flags]\n\n", name) + _, _ = fmt.Fprintf(commandStderr, "Purpose:\n %s\n\n", summary) + if len(constraints) > 0 { + _, _ = fmt.Fprintln(commandStderr, "Constraints:") + for _, constraint := range constraints { + if strings.TrimSpace(constraint) == "" { + continue + } + _, _ = fmt.Fprintf(commandStderr, " - %s\n", constraint) + } + _, _ = fmt.Fprintln(commandStderr) + } + if strings.TrimSpace(example) != "" { + _, _ = fmt.Fprintf(commandStderr, "Example:\n %s\n\n", example) + } + _, _ = fmt.Fprintln(commandStderr, "Flags:") + fs.PrintDefaults() + } +} + type gitState struct { branch string commit string diff --git a/skills/inbox/SKILL.md b/skills/inbox/SKILL.md index 5d56526..ce35d93 100644 --- a/skills/inbox/SKILL.md +++ b/skills/inbox/SKILL.md @@ -12,6 +12,7 @@ Use the bundled `./assets/inbox` CLI to communicate through a durable SQLite-bac - Invoke `./assets/inbox` relative to this skill directory. - Pass `--db` explicitly for every command. - Prefer `--json` whenever another agent or script will read the output. +- The bundled CLI help is intended to be self-describing; start with `./assets/inbox --help` or `./assets/inbox --help` when you need command usage. - Run `init` before first use on a new database path. ## Rules diff --git a/skills/inbox/assets/inbox b/skills/inbox/assets/inbox index c883222..be8f909 100755 Binary files a/skills/inbox/assets/inbox and b/skills/inbox/assets/inbox differ diff --git a/skills/orch/SKILL.md b/skills/orch/SKILL.md index 3b4059a..bbeb53e 100644 --- a/skills/orch/SKILL.md +++ b/skills/orch/SKILL.md @@ -31,6 +31,7 @@ Use the bundled `./assets/orch` CLI to control leader-side orchestration through - Invoke `./assets/orch` relative to this skill directory. - Pass `--db` explicitly for every command. - Prefer `--json` whenever another agent or script will read the output. +- The bundled CLI help is intended to be self-describing; start with `./assets/orch --help` or `./assets/orch --help` when you need command usage. - Initialize a new database path once through the bundled `inbox init` command before the first real run. - Use `status` as the main operational view. It reconciles worker thread state first, then returns run, task, latest-attempt, and latest-message context. - For Codex worker launch, standardize the handoff through `./assets/orch-worker-brief` instead of improvising the worker prompt every time. diff --git a/skills/orch/assets/orch b/skills/orch/assets/orch index f76b4cf..7a5a5c7 100755 Binary files a/skills/orch/assets/orch and b/skills/orch/assets/orch differ diff --git a/skills/repo-memory/SKILL.md b/skills/repo-memory/SKILL.md index c505171..f1da540 100644 --- a/skills/repo-memory/SKILL.md +++ b/skills/repo-memory/SKILL.md @@ -11,6 +11,7 @@ Use the bundled `./assets/repo-memory` CLI to maintain and query durable reposit - Invoke `./assets/repo-memory` relative to this skill directory. - Default database path: `~/.codex/data/repo-memory.db`. +- The bundled CLI help is intended to be self-describing; start with `./assets/repo-memory --help`, `./assets/repo-memory help `, or `./assets/repo-memory --help`. - Prefer searching before a deep repo dive so repeated work starts from existing knowledge. - Run `init` before first use on a new database path. diff --git a/skills/repo-memory/assets/repo-memory b/skills/repo-memory/assets/repo-memory index 9b221e7..131e439 100755 Binary files a/skills/repo-memory/assets/repo-memory and b/skills/repo-memory/assets/repo-memory differ