overlord-cli 3.25.0 → 4.1.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.
- package/README.md +2 -1
- package/bin/_cli/launcher.mjs +24 -5
- package/bin/_cli/new-ticket.mjs +41 -14
- package/bin/_cli/postinstall.mjs +4 -3
- package/bin/_cli/protocol.mjs +23 -6
- package/bin/_cli/setup.mjs +118 -84
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +34 -0
- package/plugins/claude/README.md +41 -0
- package/plugins/claude/commands/connect.md +18 -0
- package/plugins/claude/commands/load.md +18 -0
- package/plugins/claude/commands/spawn.md +16 -0
- package/plugins/claude/hooks/hooks.json +15 -0
- package/plugins/claude/scripts/permission-hook.sh +21 -0
- package/plugins/claude/skills/overlord-ticket-workflow/SKILL.md +104 -0
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ ovld protocol discover-project
|
|
|
37
37
|
ovld protocol attach --ticket-id <ticket-id>
|
|
38
38
|
ovld protocol update --session-key <session-key> --ticket-id <ticket-id> --summary "Working on it" --phase execute
|
|
39
39
|
ovld setup codex
|
|
40
|
+
ovld setup claude
|
|
40
41
|
ovld setup cursor
|
|
41
42
|
ovld setup gemini
|
|
42
43
|
ovld setup all
|
|
@@ -59,7 +60,7 @@ ovld doctor
|
|
|
59
60
|
- `protocol` - run ticket lifecycle commands such as `discover-project`, `attach`, `connect`, `load-context`, `spawn`, `update`, `record-change-rationales`, `ask`, `read-context`, `write-context`, `deliver`, and artifact upload/download helpers
|
|
60
61
|
- `connect`, `restart`, `context` - launch or resume an agent session or print ticket context
|
|
61
62
|
- `run`, `resume` - legacy aliases for `connect` and `restart`
|
|
62
|
-
- `setup` -
|
|
63
|
+
- `setup` - prepare the Overlord plugin or connector for a supported agent; `ovld setup claude` performs the one-time v3.25.0 to v4 Claude plugin migration
|
|
63
64
|
- `update` - install the latest CLI release from npm
|
|
64
65
|
- `doctor` - verify installed agent connectors and check whether a newer CLI version is available
|
|
65
66
|
|
package/bin/_cli/launcher.mjs
CHANGED
|
@@ -9,10 +9,25 @@ import { execFileSync } from 'node:child_process';
|
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
12
13
|
import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
|
|
13
14
|
import { runAttachCommand } from './attach.mjs';
|
|
14
15
|
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const PACKAGE_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'claude');
|
|
18
|
+
const REPO_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'claude');
|
|
19
|
+
|
|
20
|
+
function claudeSourcePluginDir() {
|
|
21
|
+
if (fs.existsSync(PACKAGE_CLAUDE_PLUGIN_DIR)) return PACKAGE_CLAUDE_PLUGIN_DIR;
|
|
22
|
+
if (fs.existsSync(REPO_CLAUDE_PLUGIN_DIR)) return REPO_CLAUDE_PLUGIN_DIR;
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
function getInstructionMode(agent) {
|
|
27
|
+
if (agent === 'claude') {
|
|
28
|
+
return claudeSourcePluginDir() ? 'bundle' : 'legacy';
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
if (agent === 'codex') {
|
|
17
32
|
const pluginManifest = path.join(
|
|
18
33
|
os.homedir(),
|
|
@@ -108,20 +123,24 @@ async function runAgent(agent, mode = 'run') {
|
|
|
108
123
|
|
|
109
124
|
try {
|
|
110
125
|
if (agent === 'claude') {
|
|
126
|
+
const pluginDir = claudeSourcePluginDir();
|
|
111
127
|
if (mode === 'resume') {
|
|
112
128
|
const claudeSessionId = process.env.CLAUDE_SESSION_ID?.trim();
|
|
113
129
|
const args = claudeSessionId
|
|
114
130
|
? ['--resume', claudeSessionId, context]
|
|
115
131
|
: ['--continue', context];
|
|
132
|
+
if (pluginDir) args.unshift('--plugin-dir', pluginDir);
|
|
116
133
|
execFileSync('claude', args, { stdio: 'inherit', env: childEnv });
|
|
117
134
|
} else {
|
|
135
|
+
const args = [
|
|
136
|
+
'--append-system-prompt',
|
|
137
|
+
context,
|
|
138
|
+
'Begin working on this ticket. Start by calling the attach endpoint, then proceed with the objective described in your system prompt.'
|
|
139
|
+
];
|
|
140
|
+
if (pluginDir) args.unshift('--plugin-dir', pluginDir);
|
|
118
141
|
execFileSync(
|
|
119
142
|
'claude',
|
|
120
|
-
|
|
121
|
-
'--append-system-prompt',
|
|
122
|
-
context,
|
|
123
|
-
'Begin working on this ticket. Start by calling the attach endpoint, then proceed with the objective described in your system prompt.'
|
|
124
|
-
],
|
|
143
|
+
args,
|
|
125
144
|
{ stdio: 'inherit', env: childEnv }
|
|
126
145
|
);
|
|
127
146
|
}
|
package/bin/_cli/new-ticket.mjs
CHANGED
|
@@ -6,7 +6,14 @@ import { stdin as input, stdout as output } from 'node:process';
|
|
|
6
6
|
import { buildAuthHeaders, resolveAuth } from './credentials.mjs';
|
|
7
7
|
import { runLauncherCommand } from './launcher.mjs';
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const PROMPT_AGENT_IDENTIFIERS = {
|
|
10
|
+
claude: 'claude-code',
|
|
11
|
+
codex: 'codex',
|
|
12
|
+
cursor: 'cursor',
|
|
13
|
+
gemini: 'gemini',
|
|
14
|
+
opencode: 'opencode'
|
|
15
|
+
};
|
|
16
|
+
const PROMPT_AGENTS = Object.keys(PROMPT_AGENT_IDENTIFIERS);
|
|
10
17
|
|
|
11
18
|
function parseFlags(args) {
|
|
12
19
|
const flags = {};
|
|
@@ -40,10 +47,10 @@ function parseFlags(args) {
|
|
|
40
47
|
|
|
41
48
|
function buildUsage(commandName) {
|
|
42
49
|
if (commandName === 'prompt') {
|
|
43
|
-
return 'Usage: ovld prompt "<objective>" [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>] [--agent <agent>]';
|
|
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>] [--delegate <agent>]';
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
return 'Usage: ovld create "<objective>" [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>]';
|
|
53
|
+
return 'Usage: ovld create "<objective>" [--title "..."] [--acceptance-criteria "..."] [--available-tools "..."] [--execution-target agent|human] [--priority low|medium|high|urgent] [--project-id <id>] [--delegate <agent>]';
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
function ensureObjective(commandName, objective) {
|
|
@@ -151,7 +158,9 @@ async function createTicket(platformUrl, agentToken, localSecret, body) {
|
|
|
151
158
|
|
|
152
159
|
const data = await res.json().catch(() => ({}));
|
|
153
160
|
if (!res.ok) {
|
|
154
|
-
throw new Error(
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Failed to create ticket (${res.status}): ${data.error ?? JSON.stringify(data)}`
|
|
163
|
+
);
|
|
155
164
|
}
|
|
156
165
|
|
|
157
166
|
return data.ticket;
|
|
@@ -179,6 +188,20 @@ function resolveAgent(agent) {
|
|
|
179
188
|
return normalizedAgent;
|
|
180
189
|
}
|
|
181
190
|
|
|
191
|
+
export function resolvePromptAgentIdentifier(agent) {
|
|
192
|
+
return PROMPT_AGENT_IDENTIFIERS[agent] ?? agent;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function resolveTicketCreationDelegate(flags = {}, selectedAgent = null) {
|
|
196
|
+
const explicitDelegate = typeof flags.delegate === 'string' ? flags.delegate.trim() : '';
|
|
197
|
+
if (explicitDelegate) return explicitDelegate;
|
|
198
|
+
|
|
199
|
+
if (selectedAgent) return resolvePromptAgentIdentifier(selectedAgent);
|
|
200
|
+
|
|
201
|
+
const envAgent = process.env.AGENT_IDENTIFIER?.trim();
|
|
202
|
+
return envAgent || null;
|
|
203
|
+
}
|
|
204
|
+
|
|
182
205
|
async function runTicketCreationFlow(args, { commandName, launchAgent }) {
|
|
183
206
|
const { flags, positionals } = parseFlags(args);
|
|
184
207
|
const objective = String(flags.objective ?? positionals.join(' ')).trim();
|
|
@@ -200,6 +223,18 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
|
|
|
200
223
|
renderItem: project => projectLabel(project)
|
|
201
224
|
}));
|
|
202
225
|
|
|
226
|
+
const selectedAgent = launchAgent
|
|
227
|
+
? (resolveAgent(typeof flags.agent === 'string' ? flags.agent : '') ??
|
|
228
|
+
(await promptForSelection({
|
|
229
|
+
items: PROMPT_AGENTS,
|
|
230
|
+
label: 'Agents',
|
|
231
|
+
prompt: 'Select an agent by number:',
|
|
232
|
+
renderItem: agent => agent
|
|
233
|
+
})))
|
|
234
|
+
: null;
|
|
235
|
+
|
|
236
|
+
const ticketDelegate = resolveTicketCreationDelegate(flags, selectedAgent);
|
|
237
|
+
|
|
203
238
|
const ticket = await createTicket(platformUrl, agentToken, localSecret, {
|
|
204
239
|
objective,
|
|
205
240
|
title: String(flags.title ?? ''),
|
|
@@ -207,7 +242,8 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
|
|
|
207
242
|
availableTools: String(flags['available-tools'] ?? ''),
|
|
208
243
|
executionTarget: String(flags['execution-target'] ?? 'agent'),
|
|
209
244
|
priority: String(flags.priority ?? 'medium'),
|
|
210
|
-
projectId: selectedProject.id
|
|
245
|
+
projectId: selectedProject.id,
|
|
246
|
+
...(ticketDelegate ? { delegate: ticketDelegate } : {})
|
|
211
247
|
});
|
|
212
248
|
|
|
213
249
|
if (!launchAgent) {
|
|
@@ -215,15 +251,6 @@ async function runTicketCreationFlow(args, { commandName, launchAgent }) {
|
|
|
215
251
|
return;
|
|
216
252
|
}
|
|
217
253
|
|
|
218
|
-
const selectedAgent =
|
|
219
|
-
resolveAgent(typeof flags.agent === 'string' ? flags.agent : '') ??
|
|
220
|
-
(await promptForSelection({
|
|
221
|
-
items: PROMPT_AGENTS,
|
|
222
|
-
label: 'Agents',
|
|
223
|
-
prompt: 'Select an agent by number:',
|
|
224
|
-
renderItem: agent => agent
|
|
225
|
-
}));
|
|
226
|
-
|
|
227
254
|
process.env.TICKET_ID = ticket.id;
|
|
228
255
|
await runLauncherCommand('run', [selectedAgent, '--ticket-id', ticket.id]);
|
|
229
256
|
}
|
package/bin/_cli/postinstall.mjs
CHANGED
|
@@ -27,15 +27,16 @@ const bold = s => `\x1b[1m${s}\x1b[0m`;
|
|
|
27
27
|
console.log(`
|
|
28
28
|
${green('✓')} Overlord CLI installed successfully!
|
|
29
29
|
|
|
30
|
-
${bold('Next step:')}
|
|
30
|
+
${bold('Next step:')} Prepare agent plugins and connectors
|
|
31
31
|
|
|
32
32
|
${cyan('ovld setup')}
|
|
33
33
|
|
|
34
34
|
This will guide you through:
|
|
35
|
-
• Selecting which agent connectors to
|
|
35
|
+
• Selecting which agent plugins/connectors to prepare (Claude, Cursor, etc.)
|
|
36
|
+
• Migrating Claude from the v3.25 connector files to the v4 plugin
|
|
36
37
|
• Configuring agent permissions for Overlord protocol access
|
|
37
38
|
|
|
38
|
-
You can also run ${cyan('ovld setup <agent>')} to
|
|
39
|
+
You can also run ${cyan('ovld setup <agent>')} to prepare a specific agent,
|
|
39
40
|
or ${cyan('ovld doctor')} to check your installation status.
|
|
40
41
|
|
|
41
42
|
Run ${cyan('ovld help')} to see all available commands.
|
package/bin/_cli/protocol.mjs
CHANGED
|
@@ -35,6 +35,22 @@ function parseFlags(args) {
|
|
|
35
35
|
return result;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export function resolveProtocolAgentIdentifier(flags = {}) {
|
|
39
|
+
const explicitAgent = typeof flags.agent === 'string' ? flags.agent.trim() : '';
|
|
40
|
+
if (explicitAgent) return explicitAgent;
|
|
41
|
+
|
|
42
|
+
const envAgent = process.env.AGENT_IDENTIFIER?.trim();
|
|
43
|
+
return envAgent || 'claude-code';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveProtocolTicketDelegate(flags = {}, agentIdentifier = '') {
|
|
47
|
+
const explicitDelegate = typeof flags.delegate === 'string' ? flags.delegate.trim() : '';
|
|
48
|
+
if (explicitDelegate) return explicitDelegate;
|
|
49
|
+
|
|
50
|
+
const resolvedAgent = String(agentIdentifier).trim();
|
|
51
|
+
return resolvedAgent || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
38
54
|
/**
|
|
39
55
|
* Default request timeout in milliseconds. Overridable via --timeout flag or
|
|
40
56
|
* OVERLORD_TIMEOUT env var. A bounded timeout prevents indefinite spinner hangs
|
|
@@ -427,7 +443,7 @@ async function protocolAttach(args) {
|
|
|
427
443
|
|
|
428
444
|
const body = {
|
|
429
445
|
ticketId,
|
|
430
|
-
agentIdentifier:
|
|
446
|
+
agentIdentifier: resolveProtocolAgentIdentifier(flags),
|
|
431
447
|
connectionMethod: String(flags.method ?? 'cli'),
|
|
432
448
|
...(externalSessionId !== undefined ? { externalSessionId } : {}),
|
|
433
449
|
metadata: {
|
|
@@ -915,7 +931,7 @@ async function protocolConnect(args) {
|
|
|
915
931
|
|
|
916
932
|
const body = {
|
|
917
933
|
ticketId,
|
|
918
|
-
agentIdentifier:
|
|
934
|
+
agentIdentifier: resolveProtocolAgentIdentifier(flags),
|
|
919
935
|
connectionMethod: String(flags.method ?? 'cli'),
|
|
920
936
|
metadata: {}
|
|
921
937
|
};
|
|
@@ -969,6 +985,7 @@ async function protocolSpawn(args) {
|
|
|
969
985
|
const objective = requireFlag(flags, 'objective', undefined);
|
|
970
986
|
const { platformUrl, agentToken, localSecret } = resolveAuth();
|
|
971
987
|
const timeoutMs = resolveTimeout(flags);
|
|
988
|
+
const agentIdentifier = resolveProtocolAgentIdentifier(flags);
|
|
972
989
|
|
|
973
990
|
// When --project-id is not provided, auto-send cwd as workingDirectory
|
|
974
991
|
// so the server can resolve the project from the local_working_directory setting.
|
|
@@ -976,7 +993,7 @@ async function protocolSpawn(args) {
|
|
|
976
993
|
|
|
977
994
|
const body = {
|
|
978
995
|
objective,
|
|
979
|
-
agentIdentifier
|
|
996
|
+
agentIdentifier,
|
|
980
997
|
connectionMethod: String(flags.method ?? 'cli'),
|
|
981
998
|
metadata: {},
|
|
982
999
|
...(flags.title ? { title: String(flags.title) } : {}),
|
|
@@ -986,7 +1003,7 @@ async function protocolSpawn(args) {
|
|
|
986
1003
|
...(flags['acceptance-criteria'] ? { acceptanceCriteria: String(flags['acceptance-criteria']) } : {}),
|
|
987
1004
|
...(flags['available-tools'] ? { availableTools: String(flags['available-tools']) } : {}),
|
|
988
1005
|
...(flags['execution-target'] ? { executionTarget: String(flags['execution-target']) } : {}),
|
|
989
|
-
|
|
1006
|
+
delegate: resolveProtocolTicketDelegate(flags, agentIdentifier),
|
|
990
1007
|
...(flags['parent-session-key'] ? { parentSessionKey: String(flags['parent-session-key']) } : {}),
|
|
991
1008
|
...(flags['parent-ticket-id'] ? { parentTicketId: String(flags['parent-ticket-id'] ?? process.env.TICKET_ID ?? '') } : {})
|
|
992
1009
|
};
|
|
@@ -1260,12 +1277,12 @@ artifact-upload-file:
|
|
|
1260
1277
|
Examples:
|
|
1261
1278
|
ovld protocol discover-project
|
|
1262
1279
|
ovld protocol discover-project --working-directory /path/to/repo
|
|
1263
|
-
ovld protocol spawn --objective "Implement feature X" # auto-resolves project from cwd
|
|
1280
|
+
ovld protocol spawn --agent codex --objective "Implement feature X" # auto-resolves project from cwd
|
|
1264
1281
|
ovld protocol attach --ticket-id abc-123
|
|
1265
1282
|
ovld protocol attach --ticket-id abc-123 --external-session-id null
|
|
1266
1283
|
ovld protocol connect --ticket-id abc-123
|
|
1267
1284
|
ovld protocol load-context --ticket-id abc-123
|
|
1268
|
-
ovld protocol spawn --objective "Implement user auth" --priority high
|
|
1285
|
+
ovld protocol spawn --agent codex --objective "Implement user auth" --priority high
|
|
1269
1286
|
ovld protocol update --session-key <key> --ticket-id <id> --summary "Did X" --phase execute
|
|
1270
1287
|
ovld protocol update --session-key <key> --ticket-id <id> --summary-file ./update.txt --event-type user_follow_up
|
|
1271
1288
|
ovld protocol record-change-rationales --session-key <key> --ticket-id <id> --change-rationales-json '[{"label":"...","file_path":"...","summary":"...","why":"...","impact":"...","hunks":[{"header":"@@ ... @@"}]}]'
|
package/bin/_cli/setup.mjs
CHANGED
|
@@ -22,6 +22,8 @@ const MANIFEST_FILE = path.join(MANIFEST_DIR, 'bundle-manifest.json');
|
|
|
22
22
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
23
|
const PACKAGE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'overlord');
|
|
24
24
|
const REPO_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'overlord');
|
|
25
|
+
const PACKAGE_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', 'plugins', 'claude');
|
|
26
|
+
const REPO_CLAUDE_PLUGIN_DIR = path.resolve(__dirname, '..', '..', '..', '..', 'plugins', 'claude');
|
|
25
27
|
const CODEX_TARGET_PLUGIN_DIR = path.join(os.homedir(), '.codex', 'plugins', 'overlord');
|
|
26
28
|
const CODEX_TARGET_PLUGIN_MANIFEST = path.join(
|
|
27
29
|
CODEX_TARGET_PLUGIN_DIR,
|
|
@@ -365,7 +367,7 @@ argument-hint: <objective or raw flags>
|
|
|
365
367
|
disable-model-invocation: true
|
|
366
368
|
---
|
|
367
369
|
|
|
368
|
-
Run \`ovld protocol spawn\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol spawn --objective "<objective>"\`.`
|
|
370
|
+
Run \`ovld protocol spawn --agent claude-code\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol spawn --agent claude-code --objective "<objective>"\`.`
|
|
369
371
|
}
|
|
370
372
|
];
|
|
371
373
|
}
|
|
@@ -386,7 +388,7 @@ Run \`ovld protocol spawn\` with \`$ARGUMENTS\`. If no flags are present, treat
|
|
|
386
388
|
{
|
|
387
389
|
path: path.join(base, 'spawn.md'),
|
|
388
390
|
content:
|
|
389
|
-
'Create a new Overlord ticket.\n\nRun `ovld protocol spawn --objective "<objective>"` using the text after `/spawn` unless raw flags were provided
|
|
391
|
+
'Create a new Overlord ticket.\n\nRun `ovld protocol spawn --agent cursor --objective "<objective>"` using the text after `/spawn` unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol spawn --agent cursor`.\n'
|
|
390
392
|
}
|
|
391
393
|
];
|
|
392
394
|
}
|
|
@@ -407,7 +409,7 @@ Run \`ovld protocol spawn\` with \`$ARGUMENTS\`. If no flags are present, treat
|
|
|
407
409
|
{
|
|
408
410
|
path: path.join(base, 'spawn.toml'),
|
|
409
411
|
content:
|
|
410
|
-
'description = "Create a new Overlord ticket from the current conversation."\nprompt = """\nRun `ovld protocol spawn --objective "<objective>"` using `{{args}}` as the objective unless raw flags were provided
|
|
412
|
+
'description = "Create a new Overlord ticket from the current conversation."\nprompt = """\nRun `ovld protocol spawn --agent gemini --objective "<objective>"` using `{{args}}` as the objective unless raw flags were provided. If raw flags were provided, pass them after `ovld protocol spawn --agent gemini`.\n"""\n'
|
|
411
413
|
}
|
|
412
414
|
];
|
|
413
415
|
}
|
|
@@ -439,7 +441,7 @@ description: Create a new Overlord ticket from the current conversation
|
|
|
439
441
|
agent: build
|
|
440
442
|
---
|
|
441
443
|
|
|
442
|
-
Run \`ovld protocol spawn\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol spawn --objective "<objective>"\`.`
|
|
444
|
+
Run \`ovld protocol spawn --agent opencode\` with \`$ARGUMENTS\`. If no flags are present, treat the arguments as the objective and call \`ovld protocol spawn --agent opencode --objective "<objective>"\`.`
|
|
443
445
|
}
|
|
444
446
|
];
|
|
445
447
|
}
|
|
@@ -459,11 +461,22 @@ function installSlashCommands(agent) {
|
|
|
459
461
|
};
|
|
460
462
|
}
|
|
461
463
|
|
|
464
|
+
function uninstallSlashCommands(agent) {
|
|
465
|
+
const removedFiles = [];
|
|
466
|
+
const files = slashCommandFiles(agent);
|
|
467
|
+
for (const file of files) {
|
|
468
|
+
if (!fs.existsSync(file.path)) continue;
|
|
469
|
+
const existing = readTextFile(file.path);
|
|
470
|
+
if (existing.trim() !== file.content.trim()) continue;
|
|
471
|
+
fs.rmSync(file.path, { force: true });
|
|
472
|
+
removedFiles.push(file.path);
|
|
473
|
+
}
|
|
474
|
+
return { removedFiles };
|
|
475
|
+
}
|
|
476
|
+
|
|
462
477
|
function currentContentHashForAgent(agent) {
|
|
463
478
|
if (agent === 'claude') {
|
|
464
|
-
return
|
|
465
|
-
[CLAUDE_SKILL_CONTENT, PERMISSION_HOOK_SCRIPT, ...slashCommandFiles('claude').map(file => file.content)].join('\n')
|
|
466
|
-
);
|
|
479
|
+
return claudeContentHash();
|
|
467
480
|
}
|
|
468
481
|
if (agent === 'cursor') {
|
|
469
482
|
return contentHash(
|
|
@@ -487,6 +500,14 @@ function codexSourcePluginDir() {
|
|
|
487
500
|
);
|
|
488
501
|
}
|
|
489
502
|
|
|
503
|
+
function claudeSourcePluginDir() {
|
|
504
|
+
if (fs.existsSync(PACKAGE_CLAUDE_PLUGIN_DIR)) return PACKAGE_CLAUDE_PLUGIN_DIR;
|
|
505
|
+
if (fs.existsSync(REPO_CLAUDE_PLUGIN_DIR)) return REPO_CLAUDE_PLUGIN_DIR;
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Claude plugin bundle not found. Checked ${PACKAGE_CLAUDE_PLUGIN_DIR} and ${REPO_CLAUDE_PLUGIN_DIR}.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
490
511
|
function listFilesRecursive(dir) {
|
|
491
512
|
if (!fs.existsSync(dir)) return [];
|
|
492
513
|
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(entry => {
|
|
@@ -496,8 +517,7 @@ function listFilesRecursive(dir) {
|
|
|
496
517
|
});
|
|
497
518
|
}
|
|
498
519
|
|
|
499
|
-
function
|
|
500
|
-
const sourceDir = codexSourcePluginDir();
|
|
520
|
+
function contentHashForDirectory(sourceDir) {
|
|
501
521
|
const hash = crypto.createHash('sha256');
|
|
502
522
|
|
|
503
523
|
for (const filePath of listFilesRecursive(sourceDir).sort()) {
|
|
@@ -510,6 +530,14 @@ function codexContentHash() {
|
|
|
510
530
|
return hash.digest('hex').slice(0, 16);
|
|
511
531
|
}
|
|
512
532
|
|
|
533
|
+
function codexContentHash() {
|
|
534
|
+
return contentHashForDirectory(codexSourcePluginDir());
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function claudeContentHash() {
|
|
538
|
+
return contentHashForDirectory(claudeSourcePluginDir());
|
|
539
|
+
}
|
|
540
|
+
|
|
513
541
|
function mergeCodexRules(existingContent) {
|
|
514
542
|
const managedBlock = [
|
|
515
543
|
CODEX_RULES_START,
|
|
@@ -621,6 +649,62 @@ function removeLegacyCodexBundle() {
|
|
|
621
649
|
writeManifest(manifest);
|
|
622
650
|
}
|
|
623
651
|
|
|
652
|
+
function removeLegacyClaudeBundle() {
|
|
653
|
+
const paths = claudePaths();
|
|
654
|
+
const removed = [];
|
|
655
|
+
|
|
656
|
+
if (fs.existsSync(paths.skillDir)) {
|
|
657
|
+
fs.rmSync(paths.skillDir, { recursive: true, force: true });
|
|
658
|
+
removed.push(paths.skillDir);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (fs.existsSync(paths.hookScript)) {
|
|
662
|
+
fs.rmSync(paths.hookScript, { force: true });
|
|
663
|
+
removed.push(paths.hookScript);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const slashResult = uninstallSlashCommands('claude');
|
|
667
|
+
removed.push(...slashResult.removedFiles);
|
|
668
|
+
|
|
669
|
+
if (fs.existsSync(paths.settingsFile)) {
|
|
670
|
+
const settings = readJsonFile(paths.settingsFile);
|
|
671
|
+
let changed = false;
|
|
672
|
+
|
|
673
|
+
if (settings.__overlord_managed) {
|
|
674
|
+
delete settings.__overlord_managed;
|
|
675
|
+
changed = true;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const hooks = settings.hooks && typeof settings.hooks === 'object' ? settings.hooks : {};
|
|
679
|
+
if (Array.isArray(hooks.PermissionRequest)) {
|
|
680
|
+
const nextPermissionHooks = hooks.PermissionRequest.filter(hook => {
|
|
681
|
+
if (hook && typeof hook === 'object' && Array.isArray(hook.hooks)) {
|
|
682
|
+
return !hook.hooks.some(
|
|
683
|
+
inner =>
|
|
684
|
+
typeof inner?.command === 'string' &&
|
|
685
|
+
inner.command.includes('overlord-permission-hook')
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
return true;
|
|
689
|
+
});
|
|
690
|
+
if (nextPermissionHooks.length !== hooks.PermissionRequest.length) {
|
|
691
|
+
changed = true;
|
|
692
|
+
if (nextPermissionHooks.length > 0) hooks.PermissionRequest = nextPermissionHooks;
|
|
693
|
+
else delete hooks.PermissionRequest;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (changed) {
|
|
698
|
+
if (Object.keys(hooks).length > 0) settings.hooks = hooks;
|
|
699
|
+
else delete settings.hooks;
|
|
700
|
+
writeJsonFile(paths.settingsFile, settings);
|
|
701
|
+
removed.push(paths.settingsFile);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return removed;
|
|
706
|
+
}
|
|
707
|
+
|
|
624
708
|
// ---------------------------------------------------------------------------
|
|
625
709
|
// Install
|
|
626
710
|
// ---------------------------------------------------------------------------
|
|
@@ -660,82 +744,30 @@ function geminiPaths() {
|
|
|
660
744
|
}
|
|
661
745
|
|
|
662
746
|
function installClaude() {
|
|
663
|
-
const
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
console.log(` ✓
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
// 3. Merge hook into settings.json
|
|
675
|
-
const backup = backupFile(paths.settingsFile);
|
|
676
|
-
if (backup) {
|
|
677
|
-
backups.push(backup);
|
|
678
|
-
console.log(` ✓ Backed up: ${paths.settingsFile} → ${path.basename(backup)}`);
|
|
747
|
+
const sourceDir = claudeSourcePluginDir();
|
|
748
|
+
const sourceManifest = path.join(sourceDir, '.claude-plugin', 'plugin.json');
|
|
749
|
+
const sourceVersion = pluginVersion(sourceManifest) ?? '0.0.0';
|
|
750
|
+
const removed = removeLegacyClaudeBundle();
|
|
751
|
+
|
|
752
|
+
console.log(` ✓ Found Claude plugin source: ${sourceDir}`);
|
|
753
|
+
if (removed.length > 0) {
|
|
754
|
+
console.log(' ✓ Migrated v3.25 Claude connector files:');
|
|
755
|
+
for (const filePath of removed) console.log(` ${filePath}`);
|
|
756
|
+
} else {
|
|
757
|
+
console.log(' ✓ No v3.25 Claude connector files needed migration.');
|
|
679
758
|
}
|
|
759
|
+
console.log(' ✓ `ovld connect claude` now loads this plugin with `claude --plugin-dir`.');
|
|
680
760
|
|
|
681
|
-
const existingSettings = readJsonFile(paths.settingsFile);
|
|
682
|
-
const overlordHook = {
|
|
683
|
-
matcher: '.*',
|
|
684
|
-
hooks: [{ type: 'command', command: paths.hookScript }]
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
const existingHooks = existingSettings.hooks ?? {};
|
|
688
|
-
const existingPermHooks = Array.isArray(existingHooks.PermissionRequest)
|
|
689
|
-
? existingHooks.PermissionRequest
|
|
690
|
-
: [];
|
|
691
|
-
const existingPermissions =
|
|
692
|
-
existingSettings.permissions && typeof existingSettings.permissions === 'object'
|
|
693
|
-
? existingSettings.permissions
|
|
694
|
-
: {};
|
|
695
|
-
const mergedAllow = Array.from(
|
|
696
|
-
new Set([
|
|
697
|
-
...asStringArray(existingPermissions.allow),
|
|
698
|
-
'Bash(ovld protocol:*)',
|
|
699
|
-
'Bash(curl -sS -X POST:*)'
|
|
700
|
-
])
|
|
701
|
-
);
|
|
702
|
-
|
|
703
|
-
// Remove existing Overlord hooks
|
|
704
|
-
const filteredPermHooks = existingPermHooks.filter(hook => {
|
|
705
|
-
if (hook && typeof hook === 'object' && hook.hooks) {
|
|
706
|
-
return !hook.hooks.some(
|
|
707
|
-
inner =>
|
|
708
|
-
typeof inner.command === 'string' && inner.command.includes('overlord-permission-hook')
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
return true;
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
const merged = deepClone(existingSettings);
|
|
715
|
-
merged.hooks = { ...existingHooks, PermissionRequest: [...filteredPermHooks, overlordHook] };
|
|
716
|
-
merged.permissions = { ...existingPermissions, allow: mergedAllow };
|
|
717
|
-
merged.__overlord_managed = {
|
|
718
|
-
version: BUNDLE_VERSION,
|
|
719
|
-
paths: ['hooks.PermissionRequest', 'permissions.allow'],
|
|
720
|
-
updatedAt: new Date().toISOString()
|
|
721
|
-
};
|
|
722
|
-
writeJsonFile(paths.settingsFile, merged);
|
|
723
|
-
console.log(` ✓ Merged hook into: ${paths.settingsFile}`);
|
|
724
|
-
|
|
725
|
-
const slashResult = installSlashCommands('claude');
|
|
726
|
-
backups.push(...slashResult.backups);
|
|
727
|
-
|
|
728
|
-
// 4. Update manifest
|
|
729
761
|
const manifest = readManifest();
|
|
730
762
|
manifest.claude = {
|
|
731
|
-
version:
|
|
763
|
+
version: sourceVersion,
|
|
732
764
|
contentHash: currentContentHashForAgent('claude'),
|
|
733
765
|
installedAt: new Date().toISOString(),
|
|
734
|
-
files:
|
|
766
|
+
files: listFilesRecursive(sourceDir)
|
|
735
767
|
};
|
|
736
768
|
writeManifest(manifest);
|
|
737
769
|
|
|
738
|
-
return { ok: true, backups };
|
|
770
|
+
return { ok: true, backups: [] };
|
|
739
771
|
}
|
|
740
772
|
|
|
741
773
|
function installOpenCode() {
|
|
@@ -923,7 +955,9 @@ function doctorAgent(agent) {
|
|
|
923
955
|
}
|
|
924
956
|
|
|
925
957
|
const currentVersion =
|
|
926
|
-
agent === '
|
|
958
|
+
agent === 'claude'
|
|
959
|
+
? pluginVersion(path.join(claudeSourcePluginDir(), '.claude-plugin', 'plugin.json'))
|
|
960
|
+
: agent === 'codex'
|
|
927
961
|
? pluginVersion(path.join(codexSourcePluginDir(), '.codex-plugin', 'plugin.json'))
|
|
928
962
|
: BUNDLE_VERSION;
|
|
929
963
|
const currentHash = currentContentHashForAgent(agent);
|
|
@@ -1346,12 +1380,12 @@ export async function runSetupCommand(args) {
|
|
|
1346
1380
|
if (agent === '--help' || agent === '-h' || agent === 'help') {
|
|
1347
1381
|
console.log(`Usage:
|
|
1348
1382
|
ovld setup Interactive setup (select agents and configure permissions)
|
|
1349
|
-
ovld setup claude
|
|
1383
|
+
ovld setup claude Prepare the Overlord Claude plugin and migrate v3.25 connector files
|
|
1350
1384
|
ovld setup codex Install Overlord Codex plugin bundle
|
|
1351
1385
|
ovld setup cursor Install Overlord rules, slash commands, and permissions for Cursor
|
|
1352
1386
|
ovld setup gemini Install Overlord slash commands and policy rules for Gemini CLI
|
|
1353
1387
|
ovld setup opencode Install Overlord connector for OpenCode
|
|
1354
|
-
ovld setup all
|
|
1388
|
+
ovld setup all Prepare all supported agents
|
|
1355
1389
|
ovld doctor Validate installed connectors and check for CLI updates`);
|
|
1356
1390
|
return;
|
|
1357
1391
|
}
|
|
@@ -1373,7 +1407,7 @@ export async function runSetupCommand(args) {
|
|
|
1373
1407
|
});
|
|
1374
1408
|
|
|
1375
1409
|
const selectedLabels = await runCheckboxPrompt({
|
|
1376
|
-
message: 'Select agent connectors to
|
|
1410
|
+
message: 'Select agent plugins/connectors to prepare (Space to toggle, Enter to confirm):',
|
|
1377
1411
|
choices: agentLabels,
|
|
1378
1412
|
defaults: []
|
|
1379
1413
|
});
|
|
@@ -1387,7 +1421,7 @@ export async function runSetupCommand(args) {
|
|
|
1387
1421
|
const selectedAgents = selectedLabels.map(label => label.split('-')[0].trim());
|
|
1388
1422
|
|
|
1389
1423
|
// Step 2: Install selected agents
|
|
1390
|
-
console.log(`\
|
|
1424
|
+
console.log(`\nPreparing Overlord agent plugins/connectors for: ${selectedAgents.join(', ')}...\n`);
|
|
1391
1425
|
|
|
1392
1426
|
const installedAgents = [];
|
|
1393
1427
|
for (const a of selectedAgents) {
|
|
@@ -1416,7 +1450,7 @@ export async function runSetupCommand(args) {
|
|
|
1416
1450
|
);
|
|
1417
1451
|
|
|
1418
1452
|
if (agentsThatNeedPermissions.length > 0) {
|
|
1419
|
-
console.log('Agent connectors
|
|
1453
|
+
console.log('Agent plugins/connectors prepared successfully!\n');
|
|
1420
1454
|
|
|
1421
1455
|
const shouldInstallPermissions = await askYesNo(
|
|
1422
1456
|
'Would you like to configure agent permissions for Overlord protocol access?',
|
|
@@ -1439,7 +1473,7 @@ export async function runSetupCommand(args) {
|
|
|
1439
1473
|
}
|
|
1440
1474
|
|
|
1441
1475
|
if (agent === 'all') {
|
|
1442
|
-
console.log('
|
|
1476
|
+
console.log('Preparing Overlord agent plugins/connectors for all supported agents...\n');
|
|
1443
1477
|
const installedAgents = [];
|
|
1444
1478
|
|
|
1445
1479
|
for (const a of supportedAgents) {
|
|
@@ -1485,7 +1519,7 @@ export async function runSetupCommand(args) {
|
|
|
1485
1519
|
process.exit(1);
|
|
1486
1520
|
}
|
|
1487
1521
|
|
|
1488
|
-
console.log(`
|
|
1522
|
+
console.log(`Preparing Overlord agent plugin/connector for ${agent}...\n`);
|
|
1489
1523
|
try {
|
|
1490
1524
|
if (agent === 'claude') installClaude();
|
|
1491
1525
|
else if (agent === 'codex') installCodex();
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "overlord",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Overlord ticket protocol workflow for Claude Code — attach/update/ask/deliver, slash commands, and a PermissionRequest notifier.",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Cooperativ",
|
|
7
|
+
"url": "https://github.com/cooperativ"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://www.ovld.ai",
|
|
10
|
+
"repository": "https://github.com/cooperativ/overlord",
|
|
11
|
+
"license": "UNLICENSED",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"overlord",
|
|
14
|
+
"tickets",
|
|
15
|
+
"agents",
|
|
16
|
+
"workflow",
|
|
17
|
+
"protocol",
|
|
18
|
+
"claude-code"
|
|
19
|
+
],
|
|
20
|
+
"userConfig": {
|
|
21
|
+
"overlord_url": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"title": "Overlord URL",
|
|
24
|
+
"description": "Overlord platform URL (e.g. https://www.ovld.ai or http://localhost:3000).",
|
|
25
|
+
"sensitive": false
|
|
26
|
+
},
|
|
27
|
+
"agent_token": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"title": "Agent token",
|
|
30
|
+
"description": "Overlord agent token. Pull from Overlord Settings → Agents & MCP or from ~/.ovld/credentials.json.",
|
|
31
|
+
"sensitive": true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Overlord — Claude Code plugin
|
|
2
|
+
|
|
3
|
+
Claude Code plugin that exposes the Overlord local ticket workflow to any Claude Code surface (CLI, VS Code extension, Claude Code desktop app).
|
|
4
|
+
|
|
5
|
+
## What ships
|
|
6
|
+
|
|
7
|
+
- `skills/overlord-ticket-workflow/SKILL.md` — durable attach → update → ask → deliver workflow.
|
|
8
|
+
- `commands/{connect,load,spawn}.md` — slash commands for session routing and ticket creation.
|
|
9
|
+
- `hooks/hooks.json` + `scripts/permission-hook.sh` — PermissionRequest notifier that posts to `/api/protocol/permission-request` on the Overlord platform.
|
|
10
|
+
- `userConfig` for `overlord_url` (non-sensitive) and `agent_token` (sensitive → OS keychain) so the hook and CLI know where to talk.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- Overlord CLI (`ovld`) on `PATH`. See the Overlord docs / Settings → Agents & MCP.
|
|
15
|
+
- `TICKET_ID` is exported automatically when a Claude session is launched from Overlord; outside that launch, set it manually before calling the permission hook.
|
|
16
|
+
|
|
17
|
+
## Install (local dev)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
claude --plugin-dir /absolute/path/to/overlord/plugins/claude
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install (marketplace)
|
|
24
|
+
|
|
25
|
+
Once published:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
claude plugin marketplace add cooperativ/overlord-marketplace
|
|
29
|
+
claude plugin install overlord@cooperativ
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The plugin prompts for `overlord_url` and `agent_token` at install time. The token is persisted to the OS keychain; the URL is stored in `~/.claude/settings.json` under `pluginConfigs["overlord"].options`.
|
|
33
|
+
|
|
34
|
+
## Namespaced components
|
|
35
|
+
|
|
36
|
+
Inside Claude Code the components are surfaced with the plugin prefix:
|
|
37
|
+
|
|
38
|
+
- skill → `overlord:overlord-ticket-workflow`
|
|
39
|
+
- commands → `/overlord:connect`, `/overlord:load`, `/overlord:spawn`
|
|
40
|
+
|
|
41
|
+
Prompts generated by Overlord (see `lib/overlord/ticket-prompt.ts`) already reference these names in `bundle` instruction mode.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Connect this session to another Overlord ticket by ticket ID
|
|
3
|
+
argument-hint: <ticket-id>
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Connect this session to another Overlord ticket.
|
|
8
|
+
|
|
9
|
+
Treat `$ARGUMENTS` as the target ticket ID.
|
|
10
|
+
If no ticket ID was provided, ask the user for one and stop.
|
|
11
|
+
|
|
12
|
+
Run:
|
|
13
|
+
`ovld protocol connect --ticket-id <ticketId>`
|
|
14
|
+
|
|
15
|
+
Rules:
|
|
16
|
+
- Use `connect`, not `attach`.
|
|
17
|
+
- Do not load extra ticket context unless the user explicitly asks for it.
|
|
18
|
+
- After the command succeeds, report the returned `SESSION_KEY` and confirm that future updates should use that ticket.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Load Overlord ticket context without creating a new session
|
|
3
|
+
argument-hint: <ticket-id>
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Load Overlord ticket context without attaching to the ticket.
|
|
8
|
+
|
|
9
|
+
Treat `$ARGUMENTS` as the target ticket ID.
|
|
10
|
+
If no ticket ID was provided, ask the user for one and stop.
|
|
11
|
+
|
|
12
|
+
Run:
|
|
13
|
+
`ovld protocol load-context --ticket-id <ticketId>`
|
|
14
|
+
|
|
15
|
+
Rules:
|
|
16
|
+
- Use `load-context`, not `attach`.
|
|
17
|
+
- Do not create or switch sessions.
|
|
18
|
+
- Summarize the returned ticket details, history, artifacts, and shared context for the user.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Create a new Overlord ticket from the current conversation
|
|
3
|
+
argument-hint: <objective or raw flags>
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Create a new Overlord ticket from the user's request.
|
|
8
|
+
|
|
9
|
+
Use `$ARGUMENTS` as the input.
|
|
10
|
+
If it already contains flags such as `--title`, `--priority`, `--project-id`, or `--execution-target`, pass those flags through after `ovld protocol spawn --agent claude-code`.
|
|
11
|
+
Otherwise, treat `$ARGUMENTS` as the objective text and run:
|
|
12
|
+
`ovld protocol spawn --agent claude-code --objective "<objective>"`
|
|
13
|
+
|
|
14
|
+
If no objective was provided, ask the user for one and stop.
|
|
15
|
+
|
|
16
|
+
After the command succeeds, report the new `TICKET_ID` and `SESSION_KEY`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Overlord PermissionRequest notification hook (plugin-managed).
|
|
3
|
+
#
|
|
4
|
+
# Prefers plugin userConfig values (CLAUDE_PLUGIN_OPTION_*) when set, and falls
|
|
5
|
+
# back to the raw OVERLORD_URL / AGENT_TOKEN env vars Overlord-launched shells
|
|
6
|
+
# already export. Silently no-ops if we can't authenticate — the hook must
|
|
7
|
+
# never block the user or leak errors into the Claude session.
|
|
8
|
+
BODY=$(cat -)
|
|
9
|
+
OVERLORD_BASE_URL="${CLAUDE_PLUGIN_OPTION_OVERLORD_URL:-$OVERLORD_URL}"
|
|
10
|
+
OVERLORD_TOKEN="${CLAUDE_PLUGIN_OPTION_AGENT_TOKEN:-$AGENT_TOKEN}"
|
|
11
|
+
if [ -n "$OVERLORD_BASE_URL" ] && [ -n "$OVERLORD_TOKEN" ] && [ -n "$TICKET_ID" ]; then
|
|
12
|
+
curl -sf -m 5 \
|
|
13
|
+
-X POST "$OVERLORD_BASE_URL/api/protocol/permission-request?ticketId=$TICKET_ID" \
|
|
14
|
+
-H "Authorization: Bearer $OVERLORD_TOKEN" \
|
|
15
|
+
-H "X-Overlord-Local-Secret: $OVERLORD_LOCAL_SECRET" \
|
|
16
|
+
-H "Content-Type: application/json" \
|
|
17
|
+
-d "$BODY" \
|
|
18
|
+
>/dev/null 2>&1 &
|
|
19
|
+
disown
|
|
20
|
+
fi
|
|
21
|
+
exit 0
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: overlord-ticket-workflow
|
|
3
|
+
description: Overlord local workflow protocol — attach, update, deliver lifecycle for ticket-driven work from Claude Code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Overlord Local Workflow
|
|
7
|
+
|
|
8
|
+
If you receive a prompt with a specified ticket ID, adhere to the following. If the prompt does not have a ticket ID, the user may choose to add one later, but otherwise, proceed without it.
|
|
9
|
+
|
|
10
|
+
## Lifecycle
|
|
11
|
+
|
|
12
|
+
1. **Attach first** — If there is a TICKET_ID, always call attach before doing any work:
|
|
13
|
+
```bash
|
|
14
|
+
ovld protocol attach --ticket-id $TICKET_ID
|
|
15
|
+
```
|
|
16
|
+
Store `session.sessionKey` from the response — it is required for all subsequent calls.
|
|
17
|
+
|
|
18
|
+
2. **Update during work** — Post at least one progress update before delivering:
|
|
19
|
+
```bash
|
|
20
|
+
ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
|
|
21
|
+
```
|
|
22
|
+
Phases: `draft`, `execute`, `review`, `deliver`, `complete`, `blocked`, `cancelled`.
|
|
23
|
+
Use `execute` while working.
|
|
24
|
+
|
|
25
|
+
Pass `--event-type <type>` to publish a specific activity event (default: `update`):
|
|
26
|
+
- `update` — standard progress update (default)
|
|
27
|
+
- `user_follow_up` — a message or question from the human user (EXCLUDING THE INITIAL TICKET)
|
|
28
|
+
- `alert` — surface a warning or non-blocking alert
|
|
29
|
+
|
|
30
|
+
3. **Ask when blocked** — Stop working after calling:
|
|
31
|
+
```bash
|
|
32
|
+
ovld protocol ask --session-key <sessionKey> --ticket-id $TICKET_ID --question "Specific question for the PM."
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
4. **Deliver last** — Always deliver when done:
|
|
36
|
+
```bash
|
|
37
|
+
ovld protocol deliver --session-key <sessionKey> \
|
|
38
|
+
--ticket-id $TICKET_ID \
|
|
39
|
+
--summary "Narrative: what you did, next steps." \
|
|
40
|
+
--artifacts-json '[{"type":"next_steps","label":"Next steps","content":"..."}]' \
|
|
41
|
+
--change-rationales-json '[{"label":"Short reviewer title","file_path":"path/to/file.ts","summary":"What changed.","why":"Why it changed.","impact":"Behavioral impact.","hunks":[{"header":"@@ -10,6 +10,14 @@"}]}]'
|
|
42
|
+
```
|
|
43
|
+
For larger delivery JSON, prefer `--payload-file -` and stream the full payload on stdin so no scratch file needs to be created or removed. If you use `--payload-file`, `--artifacts-file`, or `--change-rationales-file` with a real path, treat that file as ephemeral scratch data outside the repository and remove it after delivery. Do not leave delivery JSON checked into the worktree.
|
|
44
|
+
|
|
45
|
+
## Change Rationales
|
|
46
|
+
|
|
47
|
+
Always include `changeRationales` when delivering. Optionally include them on updates during long-running work.
|
|
48
|
+
|
|
49
|
+
Before delivering, make sure every meaningful git-tracked file change is represented in `changeRationales`; do not send `file_changes` as an artifact.
|
|
50
|
+
|
|
51
|
+
These are structured protocol payloads that Overlord stores as first-class rows in the `file_changes` table. Prefer inline JSON or the dedicated command below. For larger full delivery payloads, prefer `--payload-file -` so summary, artifacts, and change rationales stay in one JSON document without creating a temporary file. Ordinary deliver artifacts should use `next_steps`, `test_results`, `migration`, `note`, `url`, or `decision`.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ovld protocol record-change-rationales --session-key <sessionKey> --ticket-id $TICKET_ID \
|
|
55
|
+
--summary "Recorded rationale details for the latest code changes." --phase execute \
|
|
56
|
+
--change-rationales-json '[{"label":"Add backoff","file_path":"lib/api.ts","summary":"Added retry.","why":"Transient failures.","impact":"Retries 3x.","hunks":[{"header":"@@ -22,4 +22,18 @@"}]}]'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID \
|
|
61
|
+
--summary "Added retry logic." --phase execute \
|
|
62
|
+
--change-rationales-json '[{"label":"Add backoff","file_path":"lib/api.ts","summary":"Added retry.","why":"Transient failures.","impact":"Retries 3x.","hunks":[{"header":"@@ -22,4 +22,18 @@"}]}]'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Record only meaningful behavioral changes — skip formatting-only noise. Prefer 1–5 concise rationales per ticket, each tied to a specific file and diff hunk.
|
|
66
|
+
|
|
67
|
+
## Project Discovery & Ticket Spawning
|
|
68
|
+
|
|
69
|
+
When creating tickets from within a repository, `spawn` automatically resolves the
|
|
70
|
+
correct project by matching your current working directory against each project's
|
|
71
|
+
configured "Local working directory". No `--project-id` flag is needed:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
ovld protocol spawn --agent claude-code --objective "Implement feature X" --priority medium
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
To discover which project maps to the current directory:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
ovld protocol discover-project
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
You can override with `--project-id` or `--working-directory` if needed.
|
|
84
|
+
|
|
85
|
+
## Context & Artifacts
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
ovld protocol read-context --session-key <sessionKey> --ticket-id $TICKET_ID
|
|
89
|
+
ovld protocol write-context --session-key <sessionKey> --ticket-id $TICKET_ID --key "key" --value '"json-value"'
|
|
90
|
+
ovld protocol artifact-upload-file --session-key <sessionKey> --ticket-id $TICKET_ID --file ./spec.pdf --content-type application/pdf
|
|
91
|
+
ovld protocol artifact-download-url --session-key <sessionKey> --ticket-id $TICKET_ID --artifact-id <artifact-id>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Rules
|
|
95
|
+
|
|
96
|
+
- Always attach first; always deliver when done.
|
|
97
|
+
- Always communicate with Overlord using the ovld protocol cli commands.
|
|
98
|
+
- Post any substantial updates about your decisions or progress.
|
|
99
|
+
- If blocked on human-only work, call `ask` and request a follow-up human ticket.
|
|
100
|
+
- The `summary` in deliver is what the PM reads first — write it as a narrative, not a command list.
|
|
101
|
+
- Use `write-context` for facts a future agent session should know.
|
|
102
|
+
- **If the user sends you a message during your session, immediately publish a `user_follow_up` activity event with the user's message recorded verbatim in the summary before doing anything else. This DOES NOT apply to the initial ticket.**
|
|
103
|
+
- **Do not add or commit changes (git commit) unless the user explicitly asks you to commit.**
|
|
104
|
+
- **Delivery is the concluding step.** After delivering, stop working. Do not continue unless the user follows up or the ticket is reopened.
|