(rawArgs: string)
| 77 | * self-audits each turn and reports `complete`/`blocked` via UpdateGoal. |
| 78 | */ |
| 79 | export function parseGoalCommand(rawArgs: string): ParsedGoalCommand { |
| 80 | const args = rawArgs.trim(); |
| 81 | if (args.length === 0 || args === 'status') return { kind: 'status' }; |
| 82 | |
| 83 | const tokens = args.split(/\s+/); |
| 84 | const first = tokens[0]; |
| 85 | if (first === 'next') { |
| 86 | return parseNextGoalCommand(tokens); |
| 87 | } |
| 88 | if (first !== undefined && CONTROL_SUBCOMMANDS.has(first) && tokens.length === 1) { |
| 89 | return { kind: first as 'pause' | 'resume' | 'cancel' }; |
| 90 | } |
| 91 | |
| 92 | let index = 0; |
| 93 | let replace = false; |
| 94 | if (tokens[index] === 'replace') { |
| 95 | replace = true; |
| 96 | index += 1; |
| 97 | } |
| 98 | // `--` ends subcommand parsing so an objective can begin with a reserved word |
| 99 | // (e.g. `/goal -- pause the rollout`). |
| 100 | if (tokens[index] === '--') { |
| 101 | index += 1; |
| 102 | } |
| 103 | |
| 104 | const objective = tokens.slice(index).join(' ').trim(); |
| 105 | if (objective.length === 0) { |
| 106 | // A usage hint, not a failure — shown in the same calm style as the other |
| 107 | // "nothing to act on" messages (no goal to pause/resume/cancel). |
| 108 | return { |
| 109 | kind: 'error', |
| 110 | severity: 'hint', |
| 111 | message: 'Provide a goal objective, e.g. `/goal Ship feature X`.', |
| 112 | }; |
| 113 | } |
| 114 | if (objective.length > MAX_GOAL_OBJECTIVE_LENGTH) { |
| 115 | return { |
| 116 | kind: 'error', |
| 117 | message: `Goal objective is too long (max ${MAX_GOAL_OBJECTIVE_LENGTH} characters). Reference long details by file path.`, |
| 118 | }; |
| 119 | } |
| 120 | return { kind: 'create', objective, replace }; |
| 121 | } |
| 122 | |
| 123 | export async function handleGoalCommand(host: SlashCommandHost, args: string): Promise<void> { |
| 124 | const parsed = parseGoalCommand(args); |
no test coverage detected