Skip to content

Command Design Principles ​

Design guidelines for Flightdeck's agent command system. These principles ensure commands are consistent, predictable, and easy for both humans and AI agents to use.

AI-Agent Friendliness ​

All command arguments should be explicitly typed — no bare id fields. Use taskId, agentId, timerId, issueId, etc.

AI agents compose commands from memory across 46+ commands. Consistency and explicitness are more important than brevity.

Why this matters: An agent remembering that id means a task in one command and a timer in another creates confusion. When every ID is typed, the agent never has to remember exceptions. Consistency is the most AI-friendly design choice.

# ❌ Bad — what kind of ID?
CANCEL_TIMER {"id": "check-build"}
COMPLETE_TASK {"id": "rope-config"}

# ✅ Good — self-documenting
CANCEL_TIMER {"timerId": "check-build"}
COMPLETE_TASK {"taskId": "rope-config"}

Naming Conventions ​

All field names use camelCase (no snake_case).

Standard Field Names ​

FieldUsageExample
toMessaging target (agent ID or prefix)AGENT_MESSAGE {"to": "abc123", ...}
contentMessage bodyBROADCAST {"content": "Use factory pattern"}
summaryAfter-the-fact description of what happenedCOMPLETE_TASK {"summary": "Implemented auth"}
reasonJustification for an actionTERMINATE_AGENT {"reason": "need slot"}
taskWork description (what to do)DELEGATE {"task": "Fix the tests", ...}
filePathFile referenceLOCK_FILE {"filePath": "src/auth.ts"}

Typed IDs ​

IDs are always prefixed with their entity type:

FieldEntityExample
taskIdDAG taskCOMPLETE_TASK {"taskId": "rope-config"}
agentIdAgentCANCEL_DELEGATION {"agentId": "abc123"}
timerIdTimerCANCEL_TIMER {"timerId": "check-build"}
delegationIdDelegationCANCEL_DELEGATION {"delegationId": "del-123"}

Note: Groups are referenced by name (the group field), not by typed ID, since group names are human-readable identifiers.

Help Text Format ​

Command help uses a consistent format for argument visibility:

  • Required arguments: <fieldName: type> — must be provided
  • Optional arguments: [fieldName: type] or [fieldName: type = default] — can be omitted
SET_TIMER <label: string> <delay: number> <message: string> [repeat: boolean = false]

Schema-Driven Help (Zod → CommandArg) ​

Command argument metadata is derived from Zod schemas, not maintained separately. This eliminates drift between validation and documentation.

How It Works ​

Each Zod schema field uses .describe() to annotate its purpose. The deriveArgs(schema) function introspects schema.shape to auto-generate CommandArg[] for help rendering:

typescript
// Zod schema — single source of truth
const setTimerSchema = z.object({
  label: z.string().describe('Timer name'),
  delay: z.number().describe('Seconds to wait').pipe(z.number().int().positive()),
  message: z.string().describe('Message to deliver when timer fires'),
  repeat: z.boolean().optional().default(false).describe('Fire repeatedly'),
});

// Help metadata — derived, not hand-maintained
help: {
  ...deriveHelp(setTimerSchema, 'Set a reminder timer', 'Timers'),
  example: 'SET_TIMER {"label": "check-build", "delay": 300, "message": "Check CI"}',
}

deriveArgs() extracts field name, type, required/optional, description, and default value from each schema field. The deriveHelp() convenience wraps this with a command description and category.

Conventions ​

  • Every Zod schema field must have .describe('...') — drift detection tests catch missing annotations
  • Place .describe() as the last call in the chain (after .optional(), .default(), etc.) unless using .pipe() — then put .describe() before the pipe so it describes the input type
  • Use deriveHelp(schema, description, category) and spread into the command's help object
  • Keep hand-written example strings — auto-generated examples are worse than curated ones

Overrides ​

For edge cases (e.g., mutually exclusive fields), pass manual args that override the derived ones:

typescript
// CANCEL_TIMER accepts timerId OR label (mutually exclusive)
help: {
  ...deriveHelp(cancelTimerSchema, 'Cancel a timer', 'Timers'),
  args: [
    { name: 'timerId', type: 'string', required: false, description: 'Timer ID to cancel' },
    { name: 'label', type: 'string', required: false, description: 'Timer label to cancel' },
  ],
  example: 'CANCEL_TIMER {"label": "check-build"}',
}

Manual args in the help object take precedence over derived args.

Adding New Commands ​

When adding a new command:

  1. Define a Zod schema with .describe('...') on every field — drift tests enforce this
  2. Use typed IDs — never a bare id field
  3. Follow camelCase — no exceptions
  4. Reuse standard field names where they fit (to, content, summary, reason, task)
  5. Use deriveHelp() for help metadata — add a hand-written example
  6. Co-locate help with the command definition — don't maintain a separate list
Documentation generated by AI

Documentation generated by AI