cli: make bundled help self-describing
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user