overlord-cli 0.2605120852.0 → 0.2605121205.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 CHANGED
@@ -13,7 +13,7 @@ Install it globally so the `ovld` and `overlord` commands are available on your
13
13
  ```bash
14
14
  npm install -g overlord-cli
15
15
 
16
- #After installing, run
16
+ #After installing, run
17
17
  ovld setup all #to configure every supported connector (`ovld setup` alone is interactive)
18
18
 
19
19
  #for individual connectors, run `ovld setup <connector>`.
@@ -27,6 +27,7 @@ ovld setup all
27
27
  # To update the CLI to the latest release, run:
28
28
  ovld update
29
29
  ```
30
+
30
31
  ## Usage
31
32
 
32
33
  ```bash
@@ -35,6 +36,7 @@ overlord help
35
36
  ```
36
37
 
37
38
  ### Auth
39
+
38
40
  ```bash
39
41
  # Repair shared credentials first when a session already exists
40
42
  ovld auth repair # mirror and chmod shared Desktop/CLI credentials when possible
@@ -47,6 +49,7 @@ ovld auth logout # remove stored credentials
47
49
 
48
50
  Desktop-installed wrappers default `OVERLORD_URL` to `https://www.ovld.ai` unless you override it explicitly.
49
51
  For local dev against the web app on port 3000, export the override before running auth or protocol commands:
52
+
50
53
  ```bash
51
54
  export OVERLORD_URL=http://localhost:3000
52
55
  ```
@@ -101,6 +104,7 @@ Top-level commands (see `ovld help`):
101
104
  - `version` - print the installed CLI version
102
105
 
103
106
  ## Commands for agents
107
+
104
108
  Agents can find docs here: https://www.ovld.ai/docs/for-agents
105
109
 
106
110
  `ovld protocol <subcommand>` is the surface agents and hooks use for ticket lifecycle work. Run `ovld protocol help` for flags, env fallbacks, and examples.
@@ -117,6 +121,7 @@ Agents can find docs here: https://www.ovld.ai/docs/for-agents
117
121
  - `record-change-rationales` - persist structured change rationales without a normal progress update
118
122
  - `ask` - post a blocking question and move the ticket to review
119
123
  - `permission-request` - notify Overlord that the agent is requesting tool permission
124
+ - `hook-event` - record lifecycle hook events such as `UserPromptSubmit` / Cursor `beforeSubmitPrompt` without a session key
120
125
  - `read-context` - read shared persistent context for this ticket
121
126
  - `write-context` - write shared persistent context for future sessions
122
127
  - `deliver` - create a local JJ/Git checkpoint when available, send artifacts/rationales, and move the ticket to review
@@ -279,6 +279,14 @@ function readTextFile(filePath, label) {
279
279
  }
280
280
  }
281
281
 
282
+ /** Like readTextFile but accepts "-" to read from stdin. */
283
+ async function readTextFileOrStdin(filePath, label) {
284
+ if (filePath === '-') {
285
+ return readTextFromStdin(label);
286
+ }
287
+ return readTextFile(filePath, label);
288
+ }
289
+
282
290
  function readJsonFile(filePath, label) {
283
291
  try {
284
292
  return JSON.parse(readTextFile(filePath, label));
@@ -823,7 +831,7 @@ async function protocolUpdate(args) {
823
831
  if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
824
832
  if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
825
833
  const summary = flags['summary-file']
826
- ? readTextFile(String(flags['summary-file']), '--summary-file')
834
+ ? await readTextFileOrStdin(String(flags['summary-file']), '--summary-file')
827
835
  : requireFlag(flags, 'summary', undefined);
828
836
 
829
837
  const { platformUrl, bearerToken, localSecret, organizationId } =
@@ -894,7 +902,7 @@ async function protocolRecordChangeRationales(args) {
894
902
  ticketId,
895
903
  changeRationales,
896
904
  ...(flags['summary-file']
897
- ? { summary: readTextFile(String(flags['summary-file']), '--summary-file') }
905
+ ? { summary: await readTextFileOrStdin(String(flags['summary-file']), '--summary-file') }
898
906
  : flags.summary
899
907
  ? { summary: String(flags.summary) }
900
908
  : {}),
@@ -924,7 +932,7 @@ async function protocolAsk(args) {
924
932
  if (!sessionKey) throw new Error('--session-key is required (or set SESSION_KEY)');
925
933
  if (!ticketId) throw new Error('--ticket-id is required (or set TICKET_ID)');
926
934
  const question = flags['question-file']
927
- ? readTextFile(String(flags['question-file']), '--question-file')
935
+ ? await readTextFileOrStdin(String(flags['question-file']), '--question-file')
928
936
  : requireFlag(flags, 'question', undefined);
929
937
 
930
938
  const { platformUrl, bearerToken, localSecret, organizationId } =
@@ -980,6 +988,46 @@ async function protocolPermissionRequest(args) {
980
988
  console.log(JSON.stringify(data, null, 2));
981
989
  }
982
990
 
991
+ // ---------------------------------------------------------------------------
992
+ // hook-event
993
+ // ---------------------------------------------------------------------------
994
+
995
+ async function protocolHookEvent(args) {
996
+ const flags = parseFlags(args);
997
+ const ticketId = requireFlag(flags, 'ticket-id', 'TICKET_ID');
998
+ const hookType = requireFlag(flags, 'hook-type', undefined);
999
+
1000
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1001
+ await resolveProtocolAuthForFlags(flags, ticketId);
1002
+ const timeoutMs = resolveTimeout(flags);
1003
+
1004
+ const body = {
1005
+ hookType,
1006
+ ticketId,
1007
+ ...(flags.prompt !== undefined ? { prompt: String(flags.prompt) } : {}),
1008
+ ...(flags['turn-index'] !== undefined
1009
+ ? { turnIndex: parseInt(String(flags['turn-index']), 10) }
1010
+ : {})
1011
+ };
1012
+
1013
+ try {
1014
+ const data = await apiPost(
1015
+ platformUrl,
1016
+ bearerToken,
1017
+ localSecret,
1018
+ organizationId,
1019
+ '/api/protocol/hook-event',
1020
+ body,
1021
+ timeoutMs
1022
+ );
1023
+ console.log(JSON.stringify(data, null, 2));
1024
+ } catch (error) {
1025
+ const message = error instanceof Error ? error.message : String(error);
1026
+ process.stderr.write(`[protocol] hook-event skipped: ${message}\n`);
1027
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
1028
+ }
1029
+ }
1030
+
983
1031
  // ---------------------------------------------------------------------------
984
1032
  // read-context
985
1033
  // ---------------------------------------------------------------------------
@@ -1080,7 +1128,7 @@ async function protocolDeliver(args) {
1080
1128
  const summary =
1081
1129
  deliverPayload?.summary ??
1082
1130
  (flags['summary-file']
1083
- ? readTextFile(String(flags['summary-file']), '--summary-file')
1131
+ ? await readTextFileOrStdin(String(flags['summary-file']), '--summary-file')
1084
1132
  : requireFlag(flags, 'summary', undefined));
1085
1133
 
1086
1134
  const { platformUrl, bearerToken, localSecret, organizationId } =
@@ -1705,6 +1753,7 @@ Subcommands:
1705
1753
  record-change-rationales Persist structured change rationales without a progress update
1706
1754
  ask Post a blocking question and move the ticket to review
1707
1755
  permission-request Notify Overlord that the agent is requesting tool permission
1756
+ hook-event Record a lifecycle hook event without a session key
1708
1757
  read-context Read shared persistent context for this ticket
1709
1758
  write-context Write shared persistent context for future sessions
1710
1759
  deliver Finish work, send artifacts, and move the ticket to review
@@ -1808,7 +1857,7 @@ update:
1808
1857
  Required:
1809
1858
  --session-key <key>
1810
1859
  --ticket-id <ticket_id>
1811
- --summary <text> or --summary-file <path>
1860
+ --summary <text> or --summary-file <path|->
1812
1861
  Optional:
1813
1862
  --phase <status> draft | execute | review | deliver | complete | blocked | cancelled
1814
1863
  --event-type <type> update | user_follow_up | alert
@@ -1819,6 +1868,8 @@ update:
1819
1868
  --change-rationales-file <path>
1820
1869
  Notes:
1821
1870
  Use phase=execute while actively working. user_follow_up is for verbatim human follow-up messages.
1871
+ Pass --summary-file - to read the summary from stdin — avoids shell interpretation of backticks,
1872
+ quotes, or other special characters in the summary text.
1822
1873
 
1823
1874
  record-change-rationales:
1824
1875
  Purpose:
@@ -1828,7 +1879,7 @@ record-change-rationales:
1828
1879
  --ticket-id <ticket_id>
1829
1880
  --change-rationales-json <json> or --change-rationales-file <path>
1830
1881
  Optional:
1831
- --summary <text> or --summary-file <path>
1882
+ --summary <text> or --summary-file <path|->
1832
1883
  --phase <status>
1833
1884
 
1834
1885
  ask:
@@ -1837,7 +1888,7 @@ ask:
1837
1888
  Required:
1838
1889
  --session-key <key>
1839
1890
  --ticket-id <ticket_id>
1840
- --question <text> or --question-file <path>
1891
+ --question <text> or --question-file <path|->
1841
1892
  Optional:
1842
1893
  --phase <status>
1843
1894
  --payload-json <json>
@@ -1853,6 +1904,17 @@ permission-request:
1853
1904
  Optional:
1854
1905
  --payload-file <path|-> Hook JSON payload, or stdin when "-"
1855
1906
 
1907
+ hook-event:
1908
+ Purpose:
1909
+ Record a lifecycle hook event for a ticket without requiring a session key.
1910
+ This is primarily used by installed UserPromptSubmit hooks. Stop is reserved for future lifecycle hooks.
1911
+ Required:
1912
+ --hook-type <UserPromptSubmit|Stop>
1913
+ --ticket-id <ticket_id>
1914
+ Optional:
1915
+ --prompt <text>
1916
+ --turn-index <n>
1917
+
1856
1918
  read-context:
1857
1919
  Purpose:
1858
1920
  Read persistent shared context written by earlier sessions
@@ -1880,7 +1942,7 @@ deliver:
1880
1942
  Required:
1881
1943
  --session-key <key>
1882
1944
  --ticket-id <ticket_id>
1883
- --summary <text> or --summary-file <path>
1945
+ --summary <text> or --summary-file <path|->
1884
1946
  or: --payload-file <path|-> containing { summary, artifacts, changeRationales }
1885
1947
  Optional:
1886
1948
  --artifacts-json <json>
@@ -1890,6 +1952,7 @@ deliver:
1890
1952
  --skip-file-change-check Bypass local git vs changeRationales validation
1891
1953
  Notes:
1892
1954
  Use --payload-file - to read the full delivery JSON from stdin without creating a scratch file.
1955
+ Use --summary-file - to pipe just the summary text via stdin (avoids shell special-char issues).
1893
1956
  Do not combine --payload-file with --artifacts-json/--artifacts-file or change-rationale flags.
1894
1957
  In a git workspace, deliver validates that changed files are represented by changeRationales unless skipped.
1895
1958
 
@@ -2017,6 +2080,7 @@ Examples:
2017
2080
  ovld protocol update --session-key <key> --ticket-id <ticket_id> --summary-file ./update.txt --event-type user_follow_up
2018
2081
  ovld protocol record-change-rationales --session-key <key> --ticket-id <ticket_id> --change-rationales-json '[{"label":"...","file_path":"...","summary":"...","why":"...","impact":"...","hunks":[{"header":"@@ ... @@"}]}]'
2019
2082
  ovld protocol ask --session-key <key> --ticket-id <ticket_id> --question-file ./question.txt
2083
+ ovld protocol hook-event --hook-type UserPromptSubmit --ticket-id <ticket_id> --prompt "User follow-up" --turn-index 1
2020
2084
  ovld protocol read-context --session-key <key> --ticket-id <ticket_id> --query arch --limit 5
2021
2085
  ovld protocol write-context --session-key <key> --ticket-id <ticket_id> --key "arch" --value '"monorepo"' --tags repo,agent
2022
2086
  ovld protocol attachment-list --session-key <key> --ticket-id <ticket_id>
@@ -2101,6 +2165,10 @@ Examples:
2101
2165
  await protocolPermissionRequest(args);
2102
2166
  return;
2103
2167
  }
2168
+ if (subcommand === 'hook-event') {
2169
+ await protocolHookEvent(args);
2170
+ return;
2171
+ }
2104
2172
  if (subcommand === 'read-context') {
2105
2173
  await protocolReadContext(args);
2106
2174
  return;
@@ -809,10 +809,57 @@ function cursorPaths() {
809
809
  'plugin.json'
810
810
  ),
811
811
  rulesFile: path.join(base, 'rules', 'overlord-local.mdc'),
812
- settingsFile: path.join(base, 'settings.json')
812
+ settingsFile: path.join(base, 'settings.json'),
813
+ hooksFile: path.join(base, 'hooks.json')
813
814
  };
814
815
  }
815
816
 
817
+ const CURSOR_USER_PROMPT_HOOK_RELATIVE = 'plugins/local/overlord/hooks/overlord-user-prompt-submit.sh';
818
+
819
+ function isOverlordCursorBeforeSubmitHook(entry) {
820
+ if (!entry || typeof entry !== 'object') return false;
821
+ const cmd = typeof entry.command === 'string' ? entry.command : '';
822
+ return cmd.includes('overlord-user-prompt-submit');
823
+ }
824
+
825
+ function mergeCursorBeforeSubmitHook(paths) {
826
+ const hooksFile = paths.hooksFile;
827
+ const base = fs.existsSync(hooksFile)
828
+ ? readJsonFile(hooksFile)
829
+ : { version: 1, hooks: {} };
830
+ const hooks = base.hooks && typeof base.hooks === 'object' && !Array.isArray(base.hooks) ? base.hooks : {};
831
+ const existing = Array.isArray(hooks.beforeSubmitPrompt) ? hooks.beforeSubmitPrompt : [];
832
+ hooks.beforeSubmitPrompt = [
833
+ ...existing.filter(entry => !isOverlordCursorBeforeSubmitHook(entry)),
834
+ { command: CURSOR_USER_PROMPT_HOOK_RELATIVE }
835
+ ];
836
+ writeJsonFile(hooksFile, {
837
+ ...base,
838
+ version: typeof base.version === 'number' ? base.version : 1,
839
+ hooks
840
+ });
841
+ }
842
+
843
+ function removeCursorBeforeSubmitHook(paths) {
844
+ const hooksFile = paths.hooksFile;
845
+ if (!fs.existsSync(hooksFile)) return;
846
+ const base = readJsonFile(hooksFile);
847
+ const hooks = base.hooks && typeof base.hooks === 'object' && !Array.isArray(base.hooks) ? base.hooks : {};
848
+ if (!Array.isArray(hooks.beforeSubmitPrompt)) return;
849
+ hooks.beforeSubmitPrompt = hooks.beforeSubmitPrompt.filter(
850
+ entry => !isOverlordCursorBeforeSubmitHook(entry)
851
+ );
852
+ if (hooks.beforeSubmitPrompt.length === 0) {
853
+ delete hooks.beforeSubmitPrompt;
854
+ }
855
+ if (Object.keys(hooks).length === 0) {
856
+ delete base.hooks;
857
+ } else {
858
+ base.hooks = hooks;
859
+ }
860
+ writeJsonFile(hooksFile, base);
861
+ }
862
+
816
863
  function geminiPaths() {
817
864
  const base = path.join(os.homedir(), '.gemini');
818
865
  return {
@@ -945,6 +992,7 @@ function installCodex() {
945
992
 
946
993
  function installCursor() {
947
994
  const paths = cursorPaths();
995
+ const backups = [];
948
996
  const sourceDir = cursorSourcePluginDir();
949
997
  fs.mkdirSync(path.dirname(paths.pluginDir), { recursive: true });
950
998
  fs.rmSync(paths.pluginDir, { recursive: true, force: true });
@@ -961,6 +1009,14 @@ function installCursor() {
961
1009
  for (const filePath of removedLegacySlash.removedFiles) console.log(` ${filePath}`);
962
1010
  }
963
1011
 
1012
+ const hooksBackup = backupFile(paths.hooksFile);
1013
+ if (hooksBackup) {
1014
+ backups.push(hooksBackup);
1015
+ console.log(` ✓ Backed up: ${paths.hooksFile} → ${path.basename(hooksBackup)}`);
1016
+ }
1017
+ mergeCursorBeforeSubmitHook(paths);
1018
+ console.log(` ✓ Registered Cursor beforeSubmitPrompt hook: ${paths.hooksFile}`);
1019
+
964
1020
  const existingSettings = readJsonFile(paths.settingsFile);
965
1021
  const permissions =
966
1022
  existingSettings.permissions && typeof existingSettings.permissions === 'object'
@@ -983,11 +1039,11 @@ function installCursor() {
983
1039
  version: pluginVersion(paths.pluginManifest) ?? '0.0.0',
984
1040
  contentHash: currentContentHashForAgent('cursor'),
985
1041
  installedAt: new Date().toISOString(),
986
- files: [...listFilesRecursive(paths.pluginDir), paths.settingsFile]
1042
+ files: [...listFilesRecursive(paths.pluginDir), paths.settingsFile, paths.hooksFile]
987
1043
  };
988
1044
  writeManifest(manifest);
989
1045
 
990
- return { ok: true };
1046
+ return { ok: true, backups };
991
1047
  }
992
1048
 
993
1049
  function installGemini() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2605120852.0",
3
+ "version": "0.2605121205.0",
4
4
  "description": "Overlord CLI — launch AI agents on tickets from anywhere",
5
5
  "homepage": "https://www.ovld.ai/docs/",
6
6
  "type": "module",
@@ -1,7 +1,7 @@
1
1
  {
2
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.",
3
+ "version": "0.2.0",
4
+ "description": "Overlord ticket protocol workflow for Claude Code — attach/update/ask/deliver, slash commands, and hook-driven activity capture.",
5
5
  "author": {
6
6
  "name": "Cooperativ",
7
7
  "url": "https://github.com/cooperativ"
@@ -9,14 +9,8 @@
9
9
  "homepage": "https://www.ovld.ai",
10
10
  "repository": "https://github.com/cooperativ/overlord",
11
11
  "license": "UNLICENSED",
12
- "keywords": [
13
- "overlord",
14
- "tickets",
15
- "agents",
16
- "workflow",
17
- "protocol",
18
- "claude-code"
19
- ],
12
+ "keywords": ["overlord", "tickets", "agents", "workflow", "protocol", "claude-code"],
13
+ "hooks": "./hooks/hooks.json",
20
14
  "userConfig": {
21
15
  "overlord_url": {
22
16
  "type": "string",
@@ -10,6 +10,17 @@
10
10
  }
11
11
  ]
12
12
  }
13
+ ],
14
+ "UserPromptSubmit": [
15
+ {
16
+ "matcher": ".*",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "${CLAUDE_PLUGIN_ROOT}/scripts/user-prompt-submit-hook.sh"
21
+ }
22
+ ]
23
+ }
13
24
  ]
14
25
  }
15
26
  }
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+ # Overlord UserPromptSubmit hook — fires before Claude processes each user turn.
3
+
4
+ BODY=$(cat -)
5
+ OVERLORD_BASE_URL="${CLAUDE_PLUGIN_OPTION_OVERLORD_URL:-$OVERLORD_URL}"
6
+ OVERLORD_TOKEN="${CLAUDE_PLUGIN_OPTION_AGENT_TOKEN:-${CLAUDE_PLUGIN_OPTION_OVERLORD_ACCESS_TOKEN:-$OVERLORD_ACCESS_TOKEN}}"
7
+ OVERLORD_LOCAL_SECRET="${OVERLORD_LOCAL_SECRET:-}"
8
+
9
+ TURN=$(printf '%s' "$BODY" | python3 -c "import json,sys; print(json.load(sys.stdin).get('turn_number', 0))" 2>/dev/null || echo "0")
10
+ if [ "$TURN" = "0" ]; then
11
+ exit 0
12
+ fi
13
+
14
+ if [ -n "$OVERLORD_BASE_URL" ] && [ -n "$OVERLORD_TOKEN" ] && [ -n "$TICKET_ID" ]; then
15
+ PAYLOAD=$(printf '%s' "$BODY" | python3 -c "import json,sys; body=json.load(sys.stdin); print(json.dumps({'hookType': 'UserPromptSubmit', 'ticketId': __import__('os').environ['TICKET_ID'], 'prompt': body.get('prompt', ''), 'turnIndex': body.get('turn_number', 0)}))" 2>/dev/null || echo '')
16
+ if [ -n "$PAYLOAD" ]; then
17
+ curl -sf -m 5 \
18
+ -X POST "$OVERLORD_BASE_URL/api/protocol/hook-event" \
19
+ -H "Authorization: Bearer $OVERLORD_TOKEN" \
20
+ -H "X-Overlord-Local-Secret: $OVERLORD_LOCAL_SECRET" \
21
+ -H "Content-Type: application/json" \
22
+ -d "$PAYLOAD" \
23
+ >/dev/null 2>&1 &
24
+ disown
25
+ fi
26
+ fi
27
+
28
+ exit 0
@@ -15,7 +15,7 @@ Use this mode when the prompt already contains a ticket ID or explicitly says th
15
15
  2. Keep the returned `session.sessionKey` for all follow-up calls.
16
16
  3. Treat the Overlord ticket prompt as authoritative for the objective, constraints, and delivery target.
17
17
  4. Post updates while working with `ovld protocol update --phase execute`.
18
- 5. If the user sends a follow-up message after the initial ticket, publish it immediately with `--event-type user_follow_up`.
18
+ 5. Follow-up messages after the initial ticket are captured automatically by the installed `UserPromptSubmit` hook. Do not post `user_follow_up` manually unless the hook is unavailable.
19
19
  6. If blocked, call `ovld protocol ask` and stop.
20
20
  7. Deliver last with `ovld protocol deliver`, including `changeRationales` for each meaningful behavioral file change.
21
21
 
@@ -33,7 +33,16 @@ Update:
33
33
  ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary "What you did and why." --phase execute
34
34
  ```
35
35
 
36
+ When the summary contains backticks, quotes, or other special shell characters, pipe it via stdin to prevent shell interpretation:
37
+
38
+ ```bash
39
+ ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary-file - --phase execute <<'EOF'
40
+ What you did and why — including `backticks`, "quotes", and $variables are all safe here.
41
+ EOF
42
+ ```
43
+
36
44
  Supported `--phase` values:
45
+
37
46
  - `draft`
38
47
  - `execute`
39
48
  - `review`
@@ -45,8 +54,9 @@ Supported `--phase` values:
45
54
  These are hardcoded CLI-supported values for the `--phase` flag. They are not user-defined phase types.
46
55
 
47
56
  Event types:
57
+
48
58
  - `update` for standard progress updates
49
- - `user_follow_up` for human follow-up messages after the initial ticket
59
+ - `user_follow_up` for human follow-up messages after the initial ticket when the automatic hook is unavailable
50
60
  - `alert` for warnings or non-blocking issues
51
61
 
52
62
  Ask when blocked:
@@ -65,7 +75,7 @@ ovld protocol deliver --session-key <sessionKey> \
65
75
  --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 @@"}]}]'
66
76
  ```
67
77
 
68
- `ovld protocol deliver` automatically creates a local checkpoint before the API request when the workspace is JJ- or Git-managed; use `--skip-checkpoint` only when intentionally bypassing local provenance. For larger delivery payloads, prefer `--payload-file -` and stream the full JSON 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.
78
+ `ovld protocol deliver` automatically creates a local checkpoint before the API request when the workspace is JJ- or Git-managed; use `--skip-checkpoint` only when intentionally bypassing local provenance. For larger delivery payloads, prefer `--payload-file -` and stream the full JSON on stdin so no scratch file needs to be created or removed. If the summary contains special characters, use `--summary-file -` and pipe via a single-quoted heredoc (`<<'EOF'`) to prevent shell expansion. 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.
69
79
 
70
80
  ## Mode 2: Asked From Chat To Use Overlord
71
81
 
@@ -108,6 +118,7 @@ Record only meaningful behavioral changes. Skip formatting-only noise.
108
118
  ## Project Discovery And Ticket Creation
109
119
 
110
120
  When creating tickets from within a repository:
121
+
111
122
  - Prefer `create` by default for draft ticket creation.
112
123
  - Use `prompt` only when the user explicitly asks to start execution immediately.
113
124
  - Both commands can resolve the project from the current working directory; use `--working-directory` to override.
@@ -135,7 +146,7 @@ Pass `--execution-target agent` or `--execution-target human` (default: `human`)
135
146
  - **`agent`** — any task an AI agent can complete in a computer environment: coding, internet research, document editing, data analysis, automated testing, etc.
136
147
  - **`human`** — any task requiring human presence or judgment: setting credentials or tokens in a third-party UI (e.g. Vercel, AWS), sending physical mail, making a product or business decision, physical-world actions.
137
148
 
138
- When in doubt, ask yourself: *can this be done entirely inside a terminal or browser by an AI without human intervention?* If yes → `agent`. If it requires a human to log in, decide, or act in the real world → `human`.
149
+ When in doubt, ask yourself: _can this be done entirely inside a terminal or browser by an AI without human intervention?_ If yes → `agent`. If it requires a human to log in, decide, or act in the real world → `human`.
139
150
 
140
151
  ## Context And Artifacts
141
152
 
@@ -164,10 +175,11 @@ The `attach` and `load-context` responses already include `attachments` and `obj
164
175
  - Use `ovld protocol` commands and the plugin's slash commands instead of ad hoc scripts.
165
176
  - Do not invent protocol subcommands. Use `ovld protocol help` when unsure.
166
177
  - The `summary` in deliver is what the PM reads first, so write it as a narrative, not a command list.
178
+ - When a summary or question contains backticks, `$vars`, or other shell-special characters, always use `--summary-file -` (or `--question-file -`) with a single-quoted heredoc (`<<'EOF'`). Never retry by stripping or escaping content — pipe stdin instead.
167
179
  - Use `write-context` for facts a future agent session should know.
168
180
  - If a protocol or MCP call fails with auth/session errors, run `ovld auth repair` yourself before asking the user to log in again or proceed without Overlord updates.
169
181
  - If you must run `ovld auth login`, always include `--organization-id <id>` — use the organization ID from the ticket prompt context to select the organization non-interactively and avoid a blocking TTY prompt.
170
182
  - Do not add or commit changes unless the user explicitly asks you to commit.
171
183
  - Delivery is the concluding step. After delivering, stop unless the user follows up or the ticket is reopened.
172
184
 
173
- <!-- version: 0.4.7 -->
185
+ <!-- version: 0.4.8 -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Local Overlord workflow plugin for Cursor terminal agents.",
5
5
  "author": {
6
6
  "name": "Cooperativ",
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ # Overlord Cursor beforeSubmitPrompt hook — posts user composer prompts to the ticket activity feed.
3
+ # Cursor does not expose Claude's turn_number; we send a monotonic turnIndex per conversation (always >= 1)
4
+ # so POST /api/protocol/hook-event records the event (the API skips only turnIndex === 0).
5
+
6
+ BODY=$(cat -)
7
+
8
+ OVERLORD_BASE_URL="${OVERLORD_URL:-${OVERLORD_CONNECTOR_URL:-}}"
9
+ OVERLORD_TOKEN="${OVERLORD_ACCESS_TOKEN:-}"
10
+ TICKET_ID="${TICKET_ID:-}"
11
+ OVERLORD_LOCAL_SECRET="${OVERLORD_LOCAL_SECRET:-}"
12
+
13
+ if [ -n "$OVERLORD_BASE_URL" ] && [ -n "$OVERLORD_TOKEN" ] && [ -n "$TICKET_ID" ]; then
14
+ PAYLOAD=$(
15
+ printf '%s' "$BODY" | python3 -c "
16
+ import hashlib
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ try:
22
+ body = json.load(sys.stdin)
23
+ except Exception:
24
+ sys.exit(0)
25
+
26
+ text = (body.get('prompt') or body.get('message') or '').strip()
27
+ if not text:
28
+ sys.exit(0)
29
+
30
+ cid = body.get('conversation_id') or 'unknown'
31
+ state_dir = os.path.join(os.path.expanduser('~'), '.ovld', 'cursor-user-prompt-hook')
32
+ os.makedirs(state_dir, exist_ok=True)
33
+ path = os.path.join(state_dir, hashlib.sha256(cid.encode()).hexdigest())
34
+ try:
35
+ with open(path, encoding='utf-8') as handle:
36
+ n = int((handle.read() or '0').strip() or '0')
37
+ except Exception:
38
+ n = 0
39
+ n += 1
40
+ with open(path, 'w', encoding='utf-8') as handle:
41
+ handle.write(str(n))
42
+
43
+ tid = os.environ.get('TICKET_ID', '')
44
+ print(json.dumps({'hookType': 'UserPromptSubmit', 'ticketId': tid, 'prompt': text, 'turnIndex': n}))
45
+ " 2>/dev/null
46
+ )
47
+ if [ -n "$PAYLOAD" ]; then
48
+ curl -sf -m 5 \
49
+ -X POST "$OVERLORD_BASE_URL/api/protocol/hook-event" \
50
+ -H "Authorization: Bearer $OVERLORD_TOKEN" \
51
+ -H "X-Overlord-Local-Secret: $OVERLORD_LOCAL_SECRET" \
52
+ -H "Content-Type: application/json" \
53
+ -d "$PAYLOAD" \
54
+ >/dev/null 2>&1 &
55
+ disown
56
+ fi
57
+ fi
58
+
59
+ printf '%s\n' '{"continue":true}'
60
+ exit 0
@@ -13,7 +13,7 @@ Use this skill whenever Cursor needs to work with Overlord, whether the session
13
13
  2. Keep the returned `session.sessionKey` for all follow-up calls.
14
14
  3. Treat the Overlord ticket prompt as authoritative for the objective, constraints, and delivery target.
15
15
  4. Post updates while working with `ovld protocol update --phase execute`.
16
- 5. If the user sends a follow-up message after the initial ticket, publish it immediately with `--event-type user_follow_up`.
16
+ 5. Follow-up messages after the initial ticket are captured automatically by the installed Cursor `beforeSubmitPrompt` hook (see `~/.cursor/hooks.json`). Do not post `user_follow_up` manually unless the hook is unavailable.
17
17
  6. If blocked, call `ovld protocol ask` and stop.
18
18
  7. Deliver last with `ovld protocol deliver`, including `changeRationales` for each meaningful behavioral file change.
19
19
 
@@ -42,7 +42,7 @@ Supported `--phase` values:
42
42
 
43
43
  Event types:
44
44
  - `update` for standard progress updates
45
- - `user_follow_up` for human follow-up messages after the initial ticket
45
+ - `user_follow_up` — only when the Cursor `beforeSubmitPrompt` hook is unavailable; the hook normally posts follow-ups to the activity feed
46
46
  - `alert` for warnings or non-blocking issues
47
47
 
48
48
  Ask when blocked:
@@ -152,4 +152,4 @@ Objective attachment uploads also expose two-step variants — `attachment-prepa
152
152
  - If you must run `ovld auth login`, always include `--organization-id <id>` — use the organization ID from the ticket prompt context to select the organization non-interactively and avoid a blocking TTY prompt.
153
153
  - Delivery is the concluding step. After delivering, stop unless the user follows up or the ticket is reopened.
154
154
 
155
- <!-- version: 0.4.7 -->
155
+ <!-- version: 0.4.8 -->
@@ -0,0 +1,15 @@
1
+ {
2
+ "hooks": {
3
+ "UserPromptSubmit": [
4
+ {
5
+ "matcher": ".*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "${CODEX_PLUGIN_ROOT}/scripts/user-prompt-submit-hook.sh"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "overlord",
3
- "version": "0.2.1",
4
- "description": "Chat-driven access to Overlord ticket protocol workflows.",
3
+ "version": "0.2.2",
4
+ "description": "Chat-driven access to Overlord ticket protocol workflows with hook-driven activity capture.",
5
5
  "author": {
6
6
  "name": "Cooperativ",
7
7
  "url": "https://github.com/cooperativ"
@@ -11,6 +11,7 @@
11
11
  "license": "UNLICENSED",
12
12
  "keywords": ["overlord", "tickets", "agents", "workflow", "protocol", "mcp", "codex-plugin"],
13
13
  "skills": "./skills/",
14
+ "hooks": "./.codex-plugin/hooks.json",
14
15
  "mcpServers": "./.mcp.json",
15
16
  "interface": {
16
17
  "displayName": "Overlord",
@@ -35,7 +35,10 @@ const tools = [
35
35
  inputSchema: {
36
36
  type: 'object',
37
37
  properties: {
38
- working_directory: { type: 'string', description: 'Directory to match. Defaults to the current workspace.' }
38
+ working_directory: {
39
+ type: 'string',
40
+ description: 'Directory to match. Defaults to the current workspace.'
41
+ }
39
42
  }
40
43
  },
41
44
  toCliFlags: args => ({
@@ -45,17 +48,22 @@ const tools = [
45
48
  },
46
49
  {
47
50
  name: 'attach',
48
- description: 'Attach an agent session to an existing Overlord ticket and return the working context.',
51
+ description:
52
+ 'Attach an agent session to an existing Overlord ticket and return the working context.',
49
53
  inputSchema: {
50
54
  type: 'object',
51
55
  properties: {
52
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
56
+ ticket_id: {
57
+ type: 'string',
58
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
59
+ },
53
60
  agent: { type: 'string' },
54
61
  method: { type: 'string' },
55
62
  external_session_id: { type: ['string', 'null'] },
56
63
  metadata: {
57
64
  type: 'object',
58
- description: 'Optional extra metadata merged into the attach request (same as --metadata-json on the CLI).'
65
+ description:
66
+ 'Optional extra metadata merged into the attach request (same as --metadata-json on the CLI).'
59
67
  }
60
68
  },
61
69
  required: ['ticket_id']
@@ -77,12 +85,16 @@ const tools = [
77
85
  inputSchema: {
78
86
  type: 'object',
79
87
  properties: {
80
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
88
+ ticket_id: {
89
+ type: 'string',
90
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
91
+ },
81
92
  agent: { type: 'string' },
82
93
  method: { type: 'string' },
83
94
  metadata: {
84
95
  type: 'object',
85
- description: 'Optional extra metadata merged into the connect request (same as --metadata-json on the CLI).'
96
+ description:
97
+ 'Optional extra metadata merged into the connect request (same as --metadata-json on the CLI).'
86
98
  }
87
99
  },
88
100
  required: ['ticket_id']
@@ -104,7 +116,10 @@ const tools = [
104
116
  inputSchema: {
105
117
  type: 'object',
106
118
  properties: {
107
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' }
119
+ ticket_id: {
120
+ type: 'string',
121
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
122
+ }
108
123
  },
109
124
  required: ['ticket_id']
110
125
  },
@@ -124,7 +139,10 @@ const tools = [
124
139
  priority: { type: 'string', enum: ['low', 'medium', 'high', 'urgent'] },
125
140
  project_id: { type: 'string' },
126
141
  working_directory: { type: 'string' },
127
- personal: { type: 'boolean', description: 'Create without assigning a project (private ticket).' },
142
+ personal: {
143
+ type: 'boolean',
144
+ description: 'Create without assigning a project (private ticket).'
145
+ },
128
146
  acceptance_criteria: { type: 'string' },
129
147
  available_tools: { type: 'string' },
130
148
  execution_target: { type: 'string', enum: ['agent', 'human'] },
@@ -161,9 +179,15 @@ const tools = [
161
179
  type: 'object',
162
180
  properties: {
163
181
  session_key: { type: 'string' },
164
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
182
+ ticket_id: {
183
+ type: 'string',
184
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
185
+ },
165
186
  summary: { type: 'string' },
166
- phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
187
+ phase: {
188
+ type: 'string',
189
+ enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled']
190
+ },
167
191
  event_type: { type: 'string', enum: ['update', 'user_follow_up', 'alert'] },
168
192
  external_url: { type: ['string', 'null'] },
169
193
  external_session_id: { type: ['string', 'null'] },
@@ -192,9 +216,15 @@ const tools = [
192
216
  type: 'object',
193
217
  properties: {
194
218
  session_key: { type: 'string' },
195
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
219
+ ticket_id: {
220
+ type: 'string',
221
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
222
+ },
196
223
  summary: { type: 'string' },
197
- phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
224
+ phase: {
225
+ type: 'string',
226
+ enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled']
227
+ },
198
228
  change_rationales: { type: 'array' }
199
229
  },
200
230
  required: ['session_key', 'ticket_id', 'change_rationales']
@@ -208,6 +238,31 @@ const tools = [
208
238
  }),
209
239
  subcommand: 'record-change-rationales'
210
240
  },
241
+ {
242
+ name: 'record_hook_event',
243
+ description:
244
+ 'Record a hook lifecycle event for a ticket without requiring a session key. Stop is reserved for future lifecycle hooks.',
245
+ inputSchema: {
246
+ type: 'object',
247
+ properties: {
248
+ hook_type: { type: 'string', enum: ['UserPromptSubmit', 'Stop'] },
249
+ ticket_id: {
250
+ type: 'string',
251
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
252
+ },
253
+ prompt: { type: 'string' },
254
+ turn_index: { type: 'number' }
255
+ },
256
+ required: ['hook_type', 'ticket_id']
257
+ },
258
+ toCliFlags: args => ({
259
+ 'hook-type': args.hook_type,
260
+ 'ticket-id': args.ticket_id,
261
+ prompt: args.prompt,
262
+ 'turn-index': args.turn_index
263
+ }),
264
+ subcommand: 'hook-event'
265
+ },
211
266
  {
212
267
  name: 'ask',
213
268
  description: 'Send a blocking question to the human reviewer or PM.',
@@ -215,9 +270,15 @@ const tools = [
215
270
  type: 'object',
216
271
  properties: {
217
272
  session_key: { type: 'string' },
218
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
273
+ ticket_id: {
274
+ type: 'string',
275
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
276
+ },
219
277
  question: { type: 'string' },
220
- phase: { type: 'string', enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled'] },
278
+ phase: {
279
+ type: 'string',
280
+ enum: ['draft', 'execute', 'review', 'deliver', 'complete', 'blocked', 'cancelled']
281
+ },
221
282
  payload: { type: 'object' }
222
283
  },
223
284
  required: ['session_key', 'ticket_id', 'question']
@@ -238,7 +299,10 @@ const tools = [
238
299
  type: 'object',
239
300
  properties: {
240
301
  session_key: { type: 'string' },
241
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
302
+ ticket_id: {
303
+ type: 'string',
304
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
305
+ },
242
306
  query: { type: 'string' },
243
307
  limit: { type: 'number' }
244
308
  },
@@ -259,7 +323,10 @@ const tools = [
259
323
  type: 'object',
260
324
  properties: {
261
325
  session_key: { type: 'string' },
262
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
326
+ ticket_id: {
327
+ type: 'string',
328
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
329
+ },
263
330
  key: { type: 'string' },
264
331
  value: {},
265
332
  tags: { type: 'array', items: { type: 'string' } }
@@ -283,7 +350,10 @@ const tools = [
283
350
  type: 'object',
284
351
  properties: {
285
352
  session_key: { type: 'string' },
286
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
353
+ ticket_id: {
354
+ type: 'string',
355
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
356
+ },
287
357
  summary: { type: 'string' },
288
358
  artifacts: { type: 'array' },
289
359
  change_rationales: { type: 'array' },
@@ -313,9 +383,13 @@ const tools = [
313
383
  JSON.stringify({
314
384
  summary: args.summary,
315
385
  ...(Array.isArray(args.artifacts) ? { artifacts: args.artifacts } : {}),
316
- ...(Array.isArray(args.change_rationales) ? { changeRationales: args.change_rationales } : {}),
386
+ ...(Array.isArray(args.change_rationales)
387
+ ? { changeRationales: args.change_rationales }
388
+ : {}),
317
389
  ...(args.snapshot && typeof args.snapshot === 'object' ? { snapshot: args.snapshot } : {}),
318
- ...(args.checkpoint && typeof args.checkpoint === 'object' ? { checkpoint: args.checkpoint } : {})
390
+ ...(args.checkpoint && typeof args.checkpoint === 'object'
391
+ ? { checkpoint: args.checkpoint }
392
+ : {})
319
393
  }),
320
394
  subcommand: 'deliver'
321
395
  },
@@ -327,7 +401,10 @@ const tools = [
327
401
  type: 'object',
328
402
  properties: {
329
403
  session_key: { type: 'string' },
330
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
404
+ ticket_id: {
405
+ type: 'string',
406
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
407
+ },
331
408
  objective_id: { type: 'string' }
332
409
  },
333
410
  required: ['session_key', 'ticket_id']
@@ -346,7 +423,10 @@ const tools = [
346
423
  type: 'object',
347
424
  properties: {
348
425
  session_key: { type: 'string' },
349
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
426
+ ticket_id: {
427
+ type: 'string',
428
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
429
+ },
350
430
  objective_id: { type: 'string' },
351
431
  file_name: { type: 'string' },
352
432
  label: { type: 'string' },
@@ -370,12 +450,16 @@ const tools = [
370
450
  },
371
451
  {
372
452
  name: 'finalize_attachment_upload',
373
- description: 'Finalize an objective attachment after uploading bytes to the signed storage URL.',
453
+ description:
454
+ 'Finalize an objective attachment after uploading bytes to the signed storage URL.',
374
455
  inputSchema: {
375
456
  type: 'object',
376
457
  properties: {
377
458
  session_key: { type: 'string' },
378
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
459
+ ticket_id: {
460
+ type: 'string',
461
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
462
+ },
379
463
  objective_id: { type: 'string' },
380
464
  storage_path: { type: 'string' },
381
465
  label: { type: 'string' },
@@ -404,7 +488,10 @@ const tools = [
404
488
  type: 'object',
405
489
  properties: {
406
490
  session_key: { type: 'string' },
407
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
491
+ ticket_id: {
492
+ type: 'string',
493
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
494
+ },
408
495
  objective_id: { type: 'string' },
409
496
  attachment_id: { type: 'string' },
410
497
  storage_path: { type: 'string' },
@@ -424,12 +511,16 @@ const tools = [
424
511
  },
425
512
  {
426
513
  name: 'upload_attachment_file',
427
- description: 'Prepare, upload, and finalize a local file as an objective attachment in one step.',
514
+ description:
515
+ 'Prepare, upload, and finalize a local file as an objective attachment in one step.',
428
516
  inputSchema: {
429
517
  type: 'object',
430
518
  properties: {
431
519
  session_key: { type: 'string' },
432
- ticket_id: { type: 'string', description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.' },
520
+ ticket_id: {
521
+ type: 'string',
522
+ description: 'Ticket identifier (e.g. 1:899). Also accepts UUID.'
523
+ },
433
524
  objective_id: { type: 'string' },
434
525
  file: { type: 'string' },
435
526
  file_name: { type: 'string' },
@@ -516,12 +607,13 @@ function parseMessages(chunk) {
516
607
 
517
608
  const headerText = buffer.subarray(0, headerEnd).toString('utf8');
518
609
  const headers = Object.fromEntries(
519
- headerText
520
- .split('\r\n')
521
- .map(line => {
522
- const separatorIndex = line.indexOf(':');
523
- return [line.slice(0, separatorIndex).trim().toLowerCase(), line.slice(separatorIndex + 1).trim()];
524
- })
610
+ headerText.split('\r\n').map(line => {
611
+ const separatorIndex = line.indexOf(':');
612
+ return [
613
+ line.slice(0, separatorIndex).trim().toLowerCase(),
614
+ line.slice(separatorIndex + 1).trim()
615
+ ];
616
+ })
525
617
  );
526
618
 
527
619
  const contentLength = Number(headers['content-length']);
@@ -560,7 +652,9 @@ function cliArgsFromFlags(flags) {
560
652
  }
561
653
 
562
654
  const serialized =
563
- typeof value === 'string' || typeof value === 'number' ? String(value) : JSON.stringify(value);
655
+ typeof value === 'string' || typeof value === 'number'
656
+ ? String(value)
657
+ : JSON.stringify(value);
564
658
  args.push(`--${key}`, serialized);
565
659
  }
566
660
 
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+ # Overlord UserPromptSubmit hook — fires before Codex processes each user turn.
3
+
4
+ BODY=$(cat -)
5
+ OVERLORD_BASE_URL="${OVERLORD_URL:-}"
6
+ OVERLORD_TOKEN="${OVERLORD_ACCESS_TOKEN:-}"
7
+ OVERLORD_LOCAL_SECRET="${OVERLORD_LOCAL_SECRET:-}"
8
+
9
+ TURN=$(printf '%s' "$BODY" | python3 -c "import json,sys; print(json.load(sys.stdin).get('turn_number', 0))" 2>/dev/null || echo "0")
10
+ if [ "$TURN" = "0" ]; then
11
+ exit 0
12
+ fi
13
+
14
+ if [ -n "$OVERLORD_BASE_URL" ] && [ -n "$OVERLORD_TOKEN" ] && [ -n "$TICKET_ID" ]; then
15
+ PAYLOAD=$(printf '%s' "$BODY" | python3 -c "import json,sys; body=json.load(sys.stdin); print(json.dumps({'hookType': 'UserPromptSubmit', 'ticketId': __import__('os').environ['TICKET_ID'], 'prompt': body.get('prompt', ''), 'turnIndex': body.get('turn_number', 0)}))" 2>/dev/null || echo '')
16
+ if [ -n "$PAYLOAD" ]; then
17
+ curl -sf -m 5 \
18
+ -X POST "$OVERLORD_BASE_URL/api/protocol/hook-event" \
19
+ -H "Authorization: Bearer $OVERLORD_TOKEN" \
20
+ -H "X-Overlord-Local-Secret: $OVERLORD_LOCAL_SECRET" \
21
+ -H "Content-Type: application/json" \
22
+ -d "$PAYLOAD" \
23
+ >/dev/null 2>&1 &
24
+ disown
25
+ fi
26
+ fi
27
+
28
+ exit 0
@@ -13,7 +13,7 @@ Use this skill when Codex needs to work with Overlord, whether the session was l
13
13
  2. Store the returned `SESSION_KEY` or `session.sessionKey`.
14
14
  3. Treat the Overlord ticket prompt as authoritative for the specific objective and ticket-level constraints.
15
15
  4. While working, publish meaningful progress with `ovld protocol update --session-key <sessionKey> --ticket-id <ticket_id> --phase execute --summary "..."`.
16
- 5. If a later user message arrives during the ticket session, publish it immediately with `--event-type user_follow_up` before doing anything else.
16
+ 5. Follow-up messages after the initial ticket are captured automatically by the installed `UserPromptSubmit` hook. Do not post `user_follow_up` manually unless the hook is unavailable.
17
17
  6. If blocked on a human-only action, ask a precise blocking question with `ovld protocol ask` and stop.
18
18
  7. Deliver last with `ovld protocol deliver`, including meaningful `changeRationales` for every behavioral git-tracked change.
19
19
 
@@ -32,6 +32,7 @@ ovld protocol update --session-key <sessionKey> --ticket-id $TICKET_ID --summary
32
32
  ```
33
33
 
34
34
  Supported `--phase` values:
35
+
35
36
  - `draft`
36
37
  - `execute`
37
38
  - `review`
@@ -43,8 +44,9 @@ Supported `--phase` values:
43
44
  These are hardcoded CLI-supported values for the `--phase` flag. They are not user-defined phase types.
44
45
 
45
46
  Event types:
47
+
46
48
  - `update` for standard progress updates
47
- - `user_follow_up` for human follow-up messages after the initial ticket
49
+ - `user_follow_up` for human follow-up messages after the initial ticket when the automatic hook is unavailable
48
50
  - `alert` for warnings or non-blocking issues
49
51
 
50
52
  Ask when blocked:
@@ -81,6 +83,7 @@ ovld protocol deliver --session-key <sessionKey> \
81
83
  ## Project Discovery And Ticket Creation
82
84
 
83
85
  When creating tickets from within a repository:
86
+
84
87
  - Prefer `create` by default for draft ticket creation.
85
88
  - Use `prompt` only when the user explicitly asks to start execution immediately.
86
89
  - Both commands can resolve the project from the current working directory; use `--working-directory` to override.
@@ -100,7 +103,7 @@ Pass `--execution-target agent` or `--execution-target human` (default: `human`)
100
103
  - **`agent`** — any task an AI agent can complete in a computer environment: coding, internet research, document editing, data analysis, automated testing, etc.
101
104
  - **`human`** — any task requiring human presence or judgment: setting credentials or tokens in a third-party UI (e.g. Vercel, AWS), sending physical mail, making a product or business decision, physical-world actions.
102
105
 
103
- When in doubt, ask yourself: *can this be done entirely inside a terminal or browser by an AI without human intervention?* If yes → `agent`. If it requires a human to log in, decide, or act in the real world → `human`.
106
+ When in doubt, ask yourself: _can this be done entirely inside a terminal or browser by an AI without human intervention?_ If yes → `agent`. If it requires a human to log in, decide, or act in the real world → `human`.
104
107
 
105
108
  ## Change Rationales
106
109
 
@@ -155,4 +158,4 @@ The `attach` and `load-context` responses already include `attachments` and `obj
155
158
  - If a protocol or MCP call fails with auth/session errors, run `ovld auth repair` yourself before asking the user to log in again or proceed without Overlord updates.
156
159
  - If you must run `ovld auth login`, always include `--organization-id <id>` — use the organization ID from the ticket prompt context to select the organization non-interactively and avoid a blocking TTY prompt.
157
160
 
158
- <!-- version: 0.4.7 -->
161
+ <!-- version: 0.4.8 -->