overlord-cli 0.2605181821.0 → 0.2605210944.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +6 -2
  2. package/bin/_cli/attach.mjs +1 -1
  3. package/bin/_cli/launcher.mjs +45 -26
  4. package/bin/_cli/new-ticket.mjs +84 -11
  5. package/bin/_cli/protocol.mjs +99 -19
  6. package/bin/_cli/setup.mjs +250 -56
  7. package/package.json +2 -1
  8. package/plugins/antigravity/README.md +53 -0
  9. package/plugins/antigravity/commands/add-objectives.md +13 -0
  10. package/plugins/antigravity/commands/attach.md +17 -0
  11. package/plugins/antigravity/commands/connect.md +17 -0
  12. package/plugins/antigravity/commands/create.md +15 -0
  13. package/plugins/antigravity/commands/discuss-objective.md +17 -0
  14. package/plugins/antigravity/commands/load.md +17 -0
  15. package/plugins/antigravity/commands/prompt.md +15 -0
  16. package/plugins/antigravity/commands/record-work.md +26 -0
  17. package/plugins/antigravity/hooks/hooks.json +15 -0
  18. package/plugins/antigravity/plugin.json +22 -0
  19. package/plugins/antigravity/scripts/overlord-mcp.mjs +1117 -0
  20. package/plugins/antigravity/scripts/user-prompt-submit-hook.sh +105 -0
  21. package/plugins/antigravity/skills/overlord-ticket/SKILL.md +264 -0
  22. package/plugins/claude/README.md +20 -4
  23. package/plugins/claude/commands/add-objectives.md +15 -0
  24. package/plugins/claude/commands/connect.md +3 -1
  25. package/plugins/claude/commands/create.md +3 -1
  26. package/plugins/claude/commands/load.md +4 -1
  27. package/plugins/claude/commands/prompt.md +19 -0
  28. package/plugins/claude/commands/record-work.md +2 -1
  29. package/plugins/claude/commands/spawn.md +7 -4
  30. package/plugins/claude/hooks/hooks.json +1 -0
  31. package/plugins/claude/skills/overlord-ticket/SKILL.md +31 -206
  32. package/plugins/claude/skills/overlord-ticket/reference/cli.md +127 -0
  33. package/plugins/claude/skills/overlord-ticket/reference/context.md +22 -0
  34. package/plugins/claude/skills/overlord-ticket/reference/devices.md +18 -0
  35. package/plugins/claude/skills/overlord-ticket/reference/mcp.md +9 -0
  36. package/plugins/claude/skills/overlord-ticket/reference/shell-escaping.md +16 -0
  37. package/plugins/cursor/.cursor-plugin/plugin.json +13 -1
  38. package/plugins/cursor/commands/add-objectives.md +9 -0
  39. package/plugins/cursor/commands/connect.md +9 -2
  40. package/plugins/cursor/commands/create.md +2 -1
  41. package/plugins/cursor/commands/load.md +10 -2
  42. package/plugins/cursor/commands/prompt.md +12 -0
  43. package/plugins/cursor/commands/record-work.md +12 -4
  44. package/plugins/cursor/commands/spawn.md +12 -2
  45. package/plugins/cursor/rules/overlord-local.mdc +3 -1
  46. package/plugins/cursor/scripts/overlord-mcp.mjs +1 -1
  47. package/plugins/cursor/skills/overlord-ticket/SKILL.md +35 -182
  48. package/plugins/cursor/skills/overlord-ticket/reference/cli.md +127 -0
  49. package/plugins/cursor/skills/overlord-ticket/reference/context.md +22 -0
  50. package/plugins/cursor/skills/overlord-ticket/reference/devices.md +18 -0
  51. package/plugins/cursor/skills/overlord-ticket/reference/mcp.md +9 -0
  52. package/plugins/cursor/skills/overlord-ticket/reference/shell-escaping.md +16 -0
  53. package/plugins/overlord/.codex-plugin/hooks.json +13 -1
  54. package/plugins/overlord/.codex-plugin/plugin.json +4 -4
  55. package/plugins/overlord/README.md +25 -4
  56. package/plugins/overlord/scripts/overlord-mcp.mjs +291 -47
  57. package/plugins/overlord/scripts/permission-hook.sh +26 -0
  58. package/plugins/overlord/skills/overlord-ticket/SKILL.md +52 -181
  59. package/plugins/overlord/skills/overlord-ticket/reference/cli.md +127 -0
  60. package/plugins/overlord/skills/overlord-ticket/reference/context.md +22 -0
  61. package/plugins/overlord/skills/overlord-ticket/reference/devices.md +18 -0
  62. package/plugins/overlord/skills/overlord-ticket/reference/mcp.md +9 -0
  63. package/plugins/overlord/skills/overlord-ticket/reference/shell-escaping.md +16 -0
package/README.md CHANGED
@@ -72,11 +72,14 @@ ovld setup
72
72
  ovld setup codex
73
73
  ovld setup claude
74
74
  ovld setup cursor
75
- ovld setup gemini
75
+ ovld setup antigravity
76
76
  ovld setup all
77
+ ovld launch antigravity --ticket-id <ticket_id>
77
78
  ovld doctor
78
79
  ```
79
80
 
81
+ **Antigravity:** Gemini CLI is deprecated. Use `ovld setup antigravity` and `ovld launch antigravity --ticket-id <ticket_id>`. Antigravity manages model selection internally.
82
+
80
83
  For ticket-scoped protocol and launch commands, `ticket_id` values such as `1:899` carry the organization id. The CLI uses that first, then `--organization-id` for UUID compatibility, then stored auth.
81
84
 
82
85
  ## Requirements
@@ -116,6 +119,7 @@ Agents can find docs here: https://www.ovld.ai/docs/for-agents
116
119
  - `load-context` - read ticket context without creating a session
117
120
  - `revert` - restore the local working tree to an objective checkpoint after fetching its checkpoint row
118
121
  - `search-tickets` - find tickets by keyword, status, project, creator, or update date
122
+ - `add-objectives` - append ordered objectives to an existing ticket (`--objectives-json` / `--objectives-file`)
119
123
  - `create` - create a draft ticket without attaching (standalone or follow-up)
120
124
  - `prompt` - create a ticket and attach to it immediately (`spawn` is a backward-compatible alias)
121
125
  - `record-work` - record already-completed chat work as a ticket in review with a completed objective and trigger feed-post generation
@@ -136,7 +140,7 @@ Agents can find docs here: https://www.ovld.ai/docs/for-agents
136
140
 
137
141
  Devices are keyed by **(organization, user, fingerprint)** so the same physical workstation can appear once per org session.
138
142
 
139
- Use `create` for future work you want to track, `prompt` for work that should start immediately, and `record-work` for work that was already completed in chat and now needs a review ticket plus feed post.
143
+ Use `create` for future work you want to track, `prompt` for work that should start immediately, and `record-work` for work that was already completed in chat and now needs a review ticket plus feed post. Use multiple tickets when prompts represent different features or goals; use `add-objectives` or `--objectives-json '[{"objective":"Step one"},{"objective":"Step two"}]'` when prompts are sequential steps toward the same feature or goal.
140
144
 
141
145
  `ovld protocol deliver` accepts either discrete flags like `--summary` / `--artifacts-json`, an inline full payload with `--payload-json '{"summary":"...","artifacts":[...],"changeRationales":[...]}'`, or a file/stdin payload with `--payload-file <path|->`.
142
146
 
@@ -12,7 +12,7 @@
12
12
  import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
13
13
  import { runLauncherCommand } from './launcher.mjs';
14
14
 
15
- const AGENTS = ['claude', 'cursor', 'codex', 'gemini', 'opencode'];
15
+ const AGENTS = ['claude', 'cursor', 'codex', 'antigravity', 'opencode'];
16
16
  const MAX_VISIBLE = 8;
17
17
  const SEARCH_DEBOUNCE_MS = 120;
18
18
 
@@ -23,6 +23,12 @@ function claudeSourcePluginDir() {
23
23
  return null;
24
24
  }
25
25
 
26
+ function antigravityBundleInstalled() {
27
+ return fs.existsSync(
28
+ path.join(os.homedir(), '.gemini', 'antigravity-cli', 'plugins', 'plugin.json')
29
+ );
30
+ }
31
+
26
32
  function getInstructionMode(agent) {
27
33
  if (agent === 'claude') {
28
34
  return claudeSourcePluginDir() ? 'bundle' : 'legacy';
@@ -40,9 +46,27 @@ function getInstructionMode(agent) {
40
46
  return fs.existsSync(pluginManifest) ? 'bundle' : 'legacy';
41
47
  }
42
48
 
49
+ if (agent === 'antigravity') {
50
+ return antigravityBundleInstalled() ? 'bundle' : 'legacy';
51
+ }
52
+
43
53
  return 'legacy';
44
54
  }
45
55
 
56
+ function buildAgyLaunchArgv({ contextFile, mode, sessionId, extraArgs }) {
57
+ const argv = [...extraArgs, '--add-dir', os.tmpdir()];
58
+ if (mode === 'resume') {
59
+ const agySessionId = sessionId?.trim();
60
+ if (agySessionId) {
61
+ argv.push('--conversation', agySessionId);
62
+ } else {
63
+ argv.push('--continue');
64
+ }
65
+ }
66
+ argv.push('--prompt-interactive', `@${contextFile}`);
67
+ return argv;
68
+ }
69
+
46
70
  async function fetchContext(platformUrl, bearerToken, localSecret, organizationId, ticketId, agent) {
47
71
  const params = new URLSearchParams({
48
72
  context: 'cli',
@@ -67,12 +91,12 @@ const agentIdentifierMap = {
67
91
  claude: 'claude-code',
68
92
  codex: 'codex',
69
93
  cursor: 'cursor',
70
- gemini: 'gemini',
94
+ antigravity: 'antigravity',
71
95
  opencode: 'opencode',
72
96
  pi: 'pi'
73
97
  };
74
98
 
75
- const supportedAgents = ['claude', 'codex', 'cursor', 'gemini', 'opencode', 'pi'];
99
+ const supportedAgents = ['claude', 'codex', 'cursor', 'antigravity', 'opencode', 'pi'];
76
100
 
77
101
  function shellQuote(value) {
78
102
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
@@ -88,7 +112,7 @@ function buildExtraArgs(agent, options = {}) {
88
112
  ? options.flags.map(flag => String(flag).trim()).filter(Boolean)
89
113
  : [];
90
114
 
91
- if (options.model) {
115
+ if (options.model && agent !== 'antigravity') {
92
116
  args.push('--model', options.model);
93
117
  }
94
118
 
@@ -97,8 +121,8 @@ function buildExtraArgs(agent, options = {}) {
97
121
  args.push('--effort', options.thinking);
98
122
  } else if (agent === 'codex') {
99
123
  args.push('-c', `model_reasoning_effort=${toTomlString(options.thinking)}`);
100
- } else if (agent === 'gemini') {
101
- args.push('--thinking-level', options.thinking);
124
+ } else if (agent === 'antigravity') {
125
+ // Antigravity manages thinking levels internally — no launch flag.
102
126
  } else if (agent === 'pi') {
103
127
  args.push('--thinking', options.thinking);
104
128
  }
@@ -392,31 +416,24 @@ async function runAgent(agent, mode = 'run', options = {}) {
392
416
  } else {
393
417
  execFileSync('pi', [...extraArgs, context], { stdio: 'inherit', env: childEnv });
394
418
  }
395
- } else if (agent === 'gemini') {
396
- // Write context to a temp file. Passing inline content as a positional arg
397
- // causes Gemini's @-reference parser to lstat(cwd + content) when it encounters
398
- // @ symbols in the markdown (e.g. "@@ -10,6 +10,14 @@" in JSON hunk examples),
399
- // producing an ENAMETOOLONG crash. Using @file keeps the path short.
419
+ } else if (agent === 'antigravity') {
400
420
  const tag = `overlord-${ticketId.slice(-8)}-${Date.now()}`;
401
421
  const contextFile = path.join(os.tmpdir(), `${tag}-ctx.md`);
402
422
  fs.writeFileSync(contextFile, context, 'utf-8');
403
423
  setTimeout(() => { try { fs.unlinkSync(contextFile); } catch { /* already gone */ } }, 30 * 60_000).unref();
404
424
 
405
- if (mode === 'resume') {
406
- const geminiSessionId = process.env.GEMINI_SESSION_ID?.trim();
407
- const resumeTarget = geminiSessionId ?? 'latest';
408
- execFileSync(
409
- 'gemini',
410
- [...extraArgs, '--resume', resumeTarget, '--include-directories', os.tmpdir(), `@${contextFile}`],
411
- { stdio: 'inherit', env: childEnv }
412
- );
413
- } else {
414
- execFileSync(
415
- 'gemini',
416
- [...extraArgs, '--include-directories', os.tmpdir(), `@${contextFile}`],
417
- { stdio: 'inherit', env: childEnv }
418
- );
419
- }
425
+ const agySessionId =
426
+ process.env.AGY_SESSION_ID?.trim() ?? process.env.GEMINI_SESSION_ID?.trim();
427
+ execFileSync(
428
+ 'agy',
429
+ buildAgyLaunchArgv({
430
+ contextFile,
431
+ mode,
432
+ sessionId: agySessionId,
433
+ extraArgs
434
+ }),
435
+ { stdio: 'inherit', env: childEnv }
436
+ );
420
437
  }
421
438
  } catch (error) {
422
439
  const isResume = mode === 'resume';
@@ -429,7 +446,9 @@ async function runAgent(agent, mode = 'run', options = {}) {
429
446
  ? `No prior OpenCode session was found. Start one with \`ovld launch opencode --ticket-id <ticket_id>\` first.`
430
447
  : agent === 'pi'
431
448
  ? `No prior Pi session was found. Start one with \`ovld launch pi --ticket-id <ticket_id>\` first.`
432
- : '';
449
+ : agent === 'antigravity'
450
+ ? `No prior Antigravity session was found. Start one with \`ovld launch antigravity --ticket-id <ticket_id>\` first.`
451
+ : '';
433
452
  const message = error instanceof Error ? error.message : String(error);
434
453
 
435
454
  if (isResume && noSessionHint) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  import readline from 'node:readline';
4
4
  import { stdin as input, stdout as output } from 'node:process';
5
+ import fs from 'node:fs';
5
6
 
6
7
  import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
7
8
  import { runLauncherCommand } from './launcher.mjs';
@@ -10,7 +11,7 @@ const PROMPT_AGENT_IDENTIFIERS = {
10
11
  claude: 'claude-code',
11
12
  codex: 'codex',
12
13
  cursor: 'cursor',
13
- gemini: 'gemini',
14
+ antigravity: 'antigravity',
14
15
  opencode: 'opencode'
15
16
  };
16
17
  const PROMPT_AGENTS = Object.keys(PROMPT_AGENT_IDENTIFIERS);
@@ -47,10 +48,10 @@ function parseFlags(args) {
47
48
 
48
49
  function buildUsage(commandName) {
49
50
  if (commandName === 'prompt') {
50
- return 'Usage: ovld prompt "<objective>" [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>] [--agent <agent>] [--model <identifier>] [--delegate <agent>]';
51
+ return 'Usage: ovld prompt --objectives-json \'[{"objective":"..."}]\' [--objectives-file <path>] [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>] [--agent <agent>] [--model <identifier>] [--delegate <agent>]';
51
52
  }
52
53
 
53
- return 'Usage: ovld create "<objective>" [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>] [--agent <agent>] [--model <identifier>] [--delegate <agent>]';
54
+ return 'Usage: ovld create --objectives-json \'[{"objective":"..."}]\' [--objectives-file <path>] [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>] [--agent <agent>] [--model <identifier>] [--delegate <agent>]';
54
55
  }
55
56
 
56
57
  function ensureObjective(commandName, objective) {
@@ -61,6 +62,79 @@ function ensureObjective(commandName, objective) {
61
62
  process.exit(1);
62
63
  }
63
64
 
65
+ function parseJsonFlag(flagName, rawValue) {
66
+ try {
67
+ return JSON.parse(String(rawValue));
68
+ } catch (err) {
69
+ const detail = err instanceof Error ? err.message : String(err);
70
+ throw new Error(`${flagName} must be valid JSON: ${detail}`);
71
+ }
72
+ }
73
+
74
+ function readJsonFile(filePath, label) {
75
+ try {
76
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
77
+ } catch (err) {
78
+ throw new Error(
79
+ `${label}: could not read or parse "${filePath}": ${
80
+ err instanceof Error ? err.message : String(err)
81
+ }`
82
+ );
83
+ }
84
+ }
85
+
86
+ async function readTextFromStdin(label) {
87
+ const chunks = [];
88
+ try {
89
+ for await (const chunk of process.stdin) {
90
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
91
+ }
92
+ } catch (err) {
93
+ throw new Error(
94
+ `${label}: could not read stdin: ${err instanceof Error ? err.message : String(err)}`
95
+ );
96
+ }
97
+ return Buffer.concat(chunks).toString('utf8');
98
+ }
99
+
100
+ async function readJsonFileOrStdin(filePath, label) {
101
+ if (filePath !== '-') return readJsonFile(filePath, label);
102
+ try {
103
+ return JSON.parse(await readTextFromStdin(label));
104
+ } catch (err) {
105
+ throw new Error(
106
+ `${label}: could not parse stdin: ${err instanceof Error ? err.message : String(err)}`
107
+ );
108
+ }
109
+ }
110
+
111
+ async function resolveObjectiveInput(flags) {
112
+ if (flags['objectives-json'] && flags['objectives-file']) {
113
+ throw new Error('Use either --objectives-json or --objectives-file, not both');
114
+ }
115
+
116
+ if (flags['objectives-json']) {
117
+ const objectives = parseJsonFlag('--objectives-json', flags['objectives-json']);
118
+ if (!Array.isArray(objectives) || objectives.length === 0) {
119
+ throw new Error('--objectives-json must be a non-empty JSON array');
120
+ }
121
+ return { objectives };
122
+ }
123
+
124
+ if (flags['objectives-file']) {
125
+ const objectives = await readJsonFileOrStdin(
126
+ String(flags['objectives-file']),
127
+ '--objectives-file'
128
+ );
129
+ if (!Array.isArray(objectives) || objectives.length === 0) {
130
+ throw new Error('--objectives-file must contain a non-empty JSON array');
131
+ }
132
+ return { objectives };
133
+ }
134
+
135
+ throw new Error('Provide --objectives-json or --objectives-file');
136
+ }
137
+
64
138
  export function parseNumberedSelection(rawValue, count) {
65
139
  const trimmed = rawValue.trim();
66
140
  if (!trimmed) return null;
@@ -220,9 +294,8 @@ export function resolveTicketCreationDelegate(flags = {}, selectedAgent = null,
220
294
  }
221
295
 
222
296
  async function runTicketCreationFlow(args, { commandName, launchAgent }) {
223
- const { flags, positionals } = parseFlags(args);
224
- const objective = String(flags.objective ?? positionals.join(' ')).trim();
225
- ensureObjective(commandName, objective);
297
+ const { flags } = parseFlags(args);
298
+ const objectiveInput = await resolveObjectiveInput(flags);
226
299
 
227
300
  const { platformUrl, bearerToken, localSecret, organizationId } = await resolveAuth();
228
301
  const projects = await fetchProjects(platformUrl, bearerToken, localSecret, organizationId);
@@ -254,7 +327,7 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
254
327
  const ticketDelegate = resolveTicketCreationDelegate(flags, selectedAgent, modelIdentifier);
255
328
 
256
329
  const ticket = await createTicket(platformUrl, bearerToken, localSecret, organizationId, {
257
- objective,
330
+ ...objectiveInput,
258
331
  title: String(flags.title ?? ''),
259
332
  acceptanceCriteria: String(flags['acceptance-criteria'] ?? ''),
260
333
  availableTools: String(flags['available-tools'] ?? ''),
@@ -280,8 +353,8 @@ export async function runCreateCommand(args) {
280
353
  Creates a ticket after interactive numbered project selection.
281
354
 
282
355
  Examples:
283
- ovld create "Implement login page" --agent codex
284
- ovld create "Fix sync bug" --project-id <project-id>
356
+ ovld create --objectives-json '[{"objective":"Implement login page"}]'
357
+ ovld create --objectives-json '[{"objective":"Fix sync bug"}]' --project-id <project-id>
285
358
  `);
286
359
  return;
287
360
  }
@@ -296,8 +369,8 @@ export async function runPromptCommand(args) {
296
369
  Creates a ticket after interactive numbered project selection, then lets you pick an agent by number and launches it on the new ticket.
297
370
 
298
371
  Examples:
299
- ovld prompt "Implement login page"
300
- ovld prompt "Investigate flaky tests" --agent codex --model gpt-5.4
372
+ ovld prompt --objectives-json '[{"objective":"Implement login page"}]'
373
+ ovld prompt --objectives-json '[{"objective":"Investigate flaky tests"}]' --agent codex --model gpt-5.4
301
374
  `);
302
375
  return;
303
376
  }
@@ -331,6 +331,44 @@ async function readJsonFileOrStdin(filePath, label) {
331
331
  }
332
332
  }
333
333
 
334
+ async function resolveObjectivesInput(flags, { payload = null, requireSingleObjective = true } = {}) {
335
+ if (flags['objectives-json'] && flags['objectives-file']) {
336
+ throw new Error('Use either --objectives-json or --objectives-file, not both');
337
+ }
338
+ if (payload && (flags['objectives-json'] || flags['objectives-file'])) {
339
+ throw new Error('Use either payload input or --objectives-json/--objectives-file, not both');
340
+ }
341
+
342
+ if (payload?.objectives !== undefined) {
343
+ if (!Array.isArray(payload.objectives) || payload.objectives.length === 0) {
344
+ throw new Error('payload objectives must be a non-empty array');
345
+ }
346
+ return { objectives: payload.objectives };
347
+ }
348
+
349
+ if (flags['objectives-file']) {
350
+ const objectives = await readJsonFileOrStdin(String(flags['objectives-file']), '--objectives-file');
351
+ if (!Array.isArray(objectives) || objectives.length === 0) {
352
+ throw new Error('--objectives-file must contain a non-empty JSON array');
353
+ }
354
+ return { objectives };
355
+ }
356
+
357
+ if (flags['objectives-json']) {
358
+ const objectives = parseJsonFlag('--objectives-json', flags['objectives-json']);
359
+ if (!Array.isArray(objectives) || objectives.length === 0) {
360
+ throw new Error('--objectives-json must be a non-empty JSON array');
361
+ }
362
+ return { objectives };
363
+ }
364
+
365
+ if (requireSingleObjective) {
366
+ throw new Error('Provide --objectives-json or --objectives-file');
367
+ }
368
+
369
+ throw new Error('Provide --objectives-json or --objectives-file');
370
+ }
371
+
334
372
  // ---------------------------------------------------------------------------
335
373
  // changeRationales helper
336
374
  // ---------------------------------------------------------------------------
@@ -1886,7 +1924,7 @@ async function protocolRevert(args) {
1886
1924
 
1887
1925
  async function protocolPrompt(args) {
1888
1926
  const flags = parseFlags(args);
1889
- const objective = requireFlag(flags, 'objective', undefined);
1927
+ const objectiveInput = await resolveObjectivesInput(flags);
1890
1928
  const parentTicketId =
1891
1929
  flags['parent-ticket-id'] !== undefined
1892
1930
  ? String(flags['parent-ticket-id'] ?? process.env.TICKET_ID ?? '')
@@ -1905,7 +1943,7 @@ async function protocolPrompt(args) {
1905
1943
  flags['working-directory'] ?? (!flags['project-id'] && !personal ? process.cwd() : undefined);
1906
1944
 
1907
1945
  const body = {
1908
- objective,
1946
+ ...objectiveInput,
1909
1947
  agentIdentifier,
1910
1948
  connectionMethod: String(flags.method ?? 'cli'),
1911
1949
  metadata: await resolveProtocolMetadata(flags, { cwd: process.cwd() }),
@@ -1983,7 +2021,7 @@ async function protocolDiscussObjective(args) {
1983
2021
  async function protocolCreateTicket(args) {
1984
2022
  const flags = parseFlags(args);
1985
2023
  const { sessionKey, ticketId } = resolveSessionFlags(flags);
1986
- const objective = requireFlag(flags, 'objective', undefined);
2024
+ const objectiveInput = await resolveObjectivesInput(flags);
1987
2025
  const { platformUrl, bearerToken, localSecret, organizationId } =
1988
2026
  await resolveProtocolAuthForFlags(flags, ticketId);
1989
2027
  const timeoutMs = resolveTimeout(flags);
@@ -1997,7 +2035,7 @@ async function protocolCreateTicket(args) {
1997
2035
  const body = {
1998
2036
  sessionKey,
1999
2037
  ticketId,
2000
- objective,
2038
+ ...objectiveInput,
2001
2039
  ...(flags.title ? { title: String(flags.title) } : {}),
2002
2040
  ...(flags.priority ? { priority: String(flags.priority) } : {}),
2003
2041
  ...(flags['acceptance-criteria']
@@ -2034,7 +2072,7 @@ async function protocolCreateTicket(args) {
2034
2072
  : undefined;
2035
2073
 
2036
2074
  const standaloneBody = {
2037
- objective,
2075
+ ...objectiveInput,
2038
2076
  ...(flags.personal ? { personal: true } : {}),
2039
2077
  ...(!flags.personal && flags['project-id'] ? { projectId: String(flags['project-id']) } : {}),
2040
2078
  ...(standaloneWorkingDirectory ? { workingDirectory: standaloneWorkingDirectory } : {}),
@@ -2079,21 +2117,11 @@ async function protocolRecordWork(args) {
2079
2117
  if (payload && (flags.summary || flags['summary-file'])) {
2080
2118
  throw new Error('Use either payload input or --summary/--summary-file, not both');
2081
2119
  }
2082
- if (payload && (flags.objective || flags['objective-file'])) {
2083
- throw new Error('Use either payload input or --objective/--objective-file, not both');
2120
+ if (payload && (flags['objectives-json'] || flags['objectives-file'])) {
2121
+ throw new Error('Use either payload input or --objectives-json/--objectives-file, not both');
2084
2122
  }
2085
2123
 
2086
- const objective = payload
2087
- ? (() => {
2088
- const value = payload.objective;
2089
- if (typeof value !== 'string' || !value.trim()) {
2090
- throw new Error('payload input must include a non-empty objective');
2091
- }
2092
- return value;
2093
- })()
2094
- : flags['objective-file']
2095
- ? await readTextFileOrStdin(String(flags['objective-file']), '--objective-file')
2096
- : requireFlag(flags, 'objective', undefined);
2124
+ const objectiveInput = await resolveObjectivesInput(flags, { payload });
2097
2125
 
2098
2126
  const summary = payload
2099
2127
  ? (() => {
@@ -2134,7 +2162,7 @@ async function protocolRecordWork(args) {
2134
2162
  const modelIdentifier = resolveProtocolModelIdentifier(flags);
2135
2163
 
2136
2164
  const body = {
2137
- objective,
2165
+ ...objectiveInput,
2138
2166
  summary,
2139
2167
  artifacts,
2140
2168
  changeRationales,
@@ -2174,6 +2202,36 @@ async function protocolRecordWork(args) {
2174
2202
  }
2175
2203
  }
2176
2204
 
2205
+ // ---------------------------------------------------------------------------
2206
+ // add-objectives (append ordered objectives to an existing ticket)
2207
+ // ---------------------------------------------------------------------------
2208
+
2209
+ async function protocolAddObjectives(args) {
2210
+ const flags = parseFlags(args);
2211
+ const ticketId = requireFlag(flags, 'ticket-id', process.env.TICKET_ID);
2212
+ const objectiveInput = await resolveObjectivesInput(flags, { requireSingleObjective: false });
2213
+ if (!objectiveInput.objectives) {
2214
+ throw new Error('add-objectives requires --objectives-json or --objectives-file');
2215
+ }
2216
+ const { platformUrl, bearerToken, localSecret, organizationId } =
2217
+ await resolveProtocolAuthForFlags(flags, ticketId);
2218
+ const timeoutMs = resolveTimeout(flags);
2219
+
2220
+ const data = await apiPost(
2221
+ platformUrl,
2222
+ bearerToken,
2223
+ localSecret,
2224
+ organizationId,
2225
+ '/api/protocol/add-objectives',
2226
+ {
2227
+ ticketId,
2228
+ objectives: objectiveInput.objectives
2229
+ },
2230
+ timeoutMs
2231
+ );
2232
+ console.log(JSON.stringify(data, null, 2));
2233
+ }
2234
+
2177
2235
  // ---------------------------------------------------------------------------
2178
2236
  // search-tickets (find tickets by query/status/project/created_by/dates)
2179
2237
  // ---------------------------------------------------------------------------
@@ -2283,6 +2341,7 @@ Subcommands:
2283
2341
  revert Restore the local working tree to an objective checkpoint
2284
2342
  search-tickets Find tickets by keyword, status, project, creator, or update date
2285
2343
  discuss-objective Mark a draft objective as submitted (ready for review/execution)
2344
+ add-objectives Append ordered objectives to an existing ticket
2286
2345
  create Create a draft ticket without attaching (follow-up or standalone)
2287
2346
  prompt Create a ticket and attach to it immediately
2288
2347
  record-work Record completed-from-chat work as a ticket in review + feed post (no attach)
@@ -2518,6 +2577,7 @@ prompt:
2518
2577
  current working directory (matching against the caller's project_user.local_working_directory).
2519
2578
  Required:
2520
2579
  --objective <text>
2580
+ or: --objectives-json <json> / --objectives-file <path|-> with [{ "objective": "...", "title": "...", "autoAdvance": true }]
2521
2581
  Optional:
2522
2582
  --title <text>
2523
2583
  --priority <level> low | medium | high | urgent
@@ -2554,6 +2614,7 @@ create:
2554
2614
  If session flags are omitted, resolves project by working directory and creates a standalone draft.
2555
2615
  Required:
2556
2616
  --objective <text>
2617
+ or: --objectives-json <json> / --objectives-file <path|-> with [{ "objective": "...", "title": "...", "autoAdvance": true }]
2557
2618
  Optional:
2558
2619
  --session-key <key>
2559
2620
  --ticket-id <ticket_id>
@@ -2576,6 +2637,18 @@ create:
2576
2637
  Use create for future work. If the work is already complete and only needs to be recorded,
2577
2638
  use \`record-work\` instead.
2578
2639
 
2640
+ add-objectives:
2641
+ Purpose:
2642
+ Append ordered objectives to an existing ticket. Index 0 is the first newly
2643
+ added objective to execute; later indexes queue after it.
2644
+ Required:
2645
+ --ticket-id <ticket_id>
2646
+ --objectives-json <json> or --objectives-file <path|->
2647
+ Notes:
2648
+ Use this when each prompt is a sequential step toward the same feature or
2649
+ goal. Create multiple tickets instead when prompts describe different
2650
+ features or goals.
2651
+
2579
2652
  record-work:
2580
2653
  Purpose:
2581
2654
  Record work the agent already completed in a chat as a ticket in \`review\` status
@@ -2584,6 +2657,7 @@ record-work:
2584
2657
  just did" flows. Do NOT use this for in-progress work; use \`prompt\` for that.
2585
2658
  Required:
2586
2659
  --objective <text> or --objective-file <path|-> What was asked / what was done
2660
+ or: --objectives-json <json> / --objectives-file <path|->
2587
2661
  --summary <text> or --summary-file <path|-> Narrative for feed post + reviewer
2588
2662
  or: --payload-json <json>
2589
2663
  or: --payload-file <path|-> containing { objective, summary, artifacts, changeRationales }
@@ -2718,7 +2792,9 @@ Examples:
2718
2792
  ovld protocol search-tickets --query "auth refactor" --status next-up,execute --limit 10
2719
2793
  ovld protocol discuss-objective --ticket-id 1:899
2720
2794
  ovld protocol discuss-objective --ticket-id 1:899 --objective-id <objective-uuid>
2795
+ ovld protocol add-objectives --ticket-id 1:899 --objectives-json '[{"objective":"Step one"},{"objective":"Step two"}]'
2721
2796
  ovld protocol create --agent codex --objective "Capture follow-up work from this repo"
2797
+ ovld protocol create --agent codex --objectives-json '[{"objective":"Step one"},{"objective":"Step two"}]'
2722
2798
  ovld protocol create --agent codex --session-key <key> --ticket-id <ticket_id> --objective "Capture follow-up work"
2723
2799
  ovld protocol prompt --agent codex --objective "Implement user auth" --priority high
2724
2800
  ovld protocol update --session-key <key> --ticket-id <ticket_id> --summary "Did X" --phase execute
@@ -2787,6 +2863,10 @@ EOF
2787
2863
  await protocolDiscussObjective(args);
2788
2864
  return;
2789
2865
  }
2866
+ if (subcommand === 'add-objectives') {
2867
+ await protocolAddObjectives(args);
2868
+ return;
2869
+ }
2790
2870
  if (subcommand === 'create' || subcommand === 'create-ticket') {
2791
2871
  await protocolCreateTicket(args);
2792
2872
  return;