overlord-cli 0.2605131058.0 → 0.2605150751.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
@@ -117,6 +117,7 @@ Agents can find docs here: https://www.ovld.ai/docs/for-agents
117
117
  - `search-tickets` - find tickets by keyword, status, project, creator, or update date
118
118
  - `create` - create a draft ticket without attaching (standalone or follow-up)
119
119
  - `prompt` - create a ticket and attach to it immediately (`spawn` is a backward-compatible alias)
120
+ - `record-work` - record already-completed chat work as a ticket in review with a completed objective and trigger feed-post generation
120
121
  - `update` - post progress, activity events, and optional change rationales
121
122
  - `record-change-rationales` - persist structured change rationales without a normal progress update
122
123
  - `ask` - post a blocking question and move the ticket to review
@@ -130,6 +131,8 @@ Agents can find docs here: https://www.ovld.ai/docs/for-agents
130
131
  - `attachment-download-url` - get a signed download URL for an existing objective attachment
131
132
  - `attachment-upload-file` - prepare, upload, and finalize a local objective attachment in one command
132
133
 
134
+ Use `create` for future work you want to track, `prompt` for work that should start immediately, and `record-work` for work that was already completed in chat and now needs a review ticket plus feed post.
135
+
133
136
  `ovld protocol deliver` accepts either discrete flags like `--summary` / `--artifacts-json`, an inline full payload with `--payload-json '{"summary":"...","artifacts":[...],"changeRationales":[...]}'`, or a file/stdin payload with `--payload-file <path|->`.
134
137
 
135
138
  ## License
@@ -1570,6 +1570,34 @@ async function protocolPrompt(args) {
1570
1570
  }
1571
1571
  }
1572
1572
 
1573
+ // ---------------------------------------------------------------------------
1574
+ // discuss-objective (mark a draft objective as submitted)
1575
+ // ---------------------------------------------------------------------------
1576
+
1577
+ async function protocolDiscussObjective(args) {
1578
+ const flags = parseFlags(args);
1579
+ const ticketId = requireFlag(flags, 'ticket-id', process.env.TICKET_ID);
1580
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1581
+ await resolveProtocolAuthForFlags(flags, ticketId);
1582
+ const timeoutMs = resolveTimeout(flags);
1583
+
1584
+ const body = {
1585
+ ticketId,
1586
+ ...(flags['objective-id'] ? { objectiveId: String(flags['objective-id']) } : {})
1587
+ };
1588
+
1589
+ const data = await apiPost(
1590
+ platformUrl,
1591
+ bearerToken,
1592
+ localSecret,
1593
+ organizationId,
1594
+ '/api/protocol/discuss-objective',
1595
+ body,
1596
+ timeoutMs
1597
+ );
1598
+ console.log(JSON.stringify(data, null, 2));
1599
+ }
1600
+
1573
1601
  // ---------------------------------------------------------------------------
1574
1602
  // create (create follow-up ticket draft only)
1575
1603
  // ---------------------------------------------------------------------------
@@ -1654,6 +1682,120 @@ async function protocolCreateTicket(args) {
1654
1682
  console.log(JSON.stringify(data, null, 2));
1655
1683
  }
1656
1684
 
1685
+ // ---------------------------------------------------------------------------
1686
+ // record-work (one-shot: create ticket in review + completed objective + feed post)
1687
+ // ---------------------------------------------------------------------------
1688
+
1689
+ async function protocolRecordWork(args) {
1690
+ const flags = parseFlags(args);
1691
+
1692
+ if (flags['payload-file'] && flags['payload-json']) {
1693
+ throw new Error('Use either --payload-file or --payload-json, not both');
1694
+ }
1695
+ const payload = flags['payload-file']
1696
+ ? await readJsonFileOrStdin(String(flags['payload-file']), '--payload-file')
1697
+ : flags['payload-json']
1698
+ ? parseJsonFlag('--payload-json', flags['payload-json'])
1699
+ : null;
1700
+
1701
+ if (payload && (flags.summary || flags['summary-file'])) {
1702
+ throw new Error('Use either payload input or --summary/--summary-file, not both');
1703
+ }
1704
+ if (payload && (flags.objective || flags['objective-file'])) {
1705
+ throw new Error('Use either payload input or --objective/--objective-file, not both');
1706
+ }
1707
+
1708
+ const objective = payload
1709
+ ? (() => {
1710
+ const value = payload.objective;
1711
+ if (typeof value !== 'string' || !value.trim()) {
1712
+ throw new Error('payload input must include a non-empty objective');
1713
+ }
1714
+ return value;
1715
+ })()
1716
+ : flags['objective-file']
1717
+ ? await readTextFileOrStdin(String(flags['objective-file']), '--objective-file')
1718
+ : requireFlag(flags, 'objective', undefined);
1719
+
1720
+ const summary = payload
1721
+ ? (() => {
1722
+ const value = payload.summary;
1723
+ if (typeof value !== 'string' || !value.trim()) {
1724
+ throw new Error('payload input must include a non-empty summary');
1725
+ }
1726
+ return value;
1727
+ })()
1728
+ : flags['summary-file']
1729
+ ? await readTextFileOrStdin(String(flags['summary-file']), '--summary-file')
1730
+ : requireFlag(flags, 'summary', undefined);
1731
+
1732
+ let artifacts = payload?.artifacts ?? [];
1733
+ if (payload && (flags['artifacts-file'] || flags['artifacts-json'])) {
1734
+ throw new Error('Use either payload input or artifact flags, not both');
1735
+ }
1736
+ if (flags['artifacts-file']) {
1737
+ artifacts = await readJsonFileOrStdin(String(flags['artifacts-file']), '--artifacts-file');
1738
+ } else if (flags['artifacts-json']) {
1739
+ artifacts = parseJsonFlag('--artifacts-json', flags['artifacts-json']);
1740
+ }
1741
+
1742
+ if (payload && (flags['change-rationales-file'] || flags['change-rationales-json'])) {
1743
+ throw new Error('Use either payload input or change-rationale flags, not both');
1744
+ }
1745
+ const changeRationales = payload?.changeRationales ?? (await resolveChangeRationales(flags));
1746
+
1747
+ // record-work has no live session, but the same git-changed-files guard
1748
+ // helps catch missing rationales. Apply the same opt-out semantics as deliver.
1749
+ validateDeliverFileChanges(flags, changeRationales);
1750
+
1751
+ const personal = Boolean(flags.personal);
1752
+ const workingDirectory =
1753
+ flags['working-directory'] ?? (!flags['project-id'] && !personal ? process.cwd() : undefined);
1754
+
1755
+ const agentIdentifier = resolveProtocolAgentIdentifier(flags);
1756
+ const modelIdentifier = resolveProtocolModelIdentifier(flags);
1757
+
1758
+ const body = {
1759
+ objective,
1760
+ summary,
1761
+ artifacts,
1762
+ changeRationales,
1763
+ agentIdentifier,
1764
+ connectionMethod: String(flags.method ?? 'cli'),
1765
+ metadata: await resolveProtocolMetadata(flags, { cwd: process.cwd() }),
1766
+ ...(flags.title ? { title: String(flags.title) } : {}),
1767
+ ...(flags.priority ? { priority: String(flags.priority) } : {}),
1768
+ ...(flags['project-id'] ? { projectId: String(flags['project-id']) } : {}),
1769
+ ...(personal ? { personal: true } : {}),
1770
+ ...(workingDirectory ? { workingDirectory: String(workingDirectory) } : {}),
1771
+ ...(flags['acceptance-criteria']
1772
+ ? { acceptanceCriteria: String(flags['acceptance-criteria']) }
1773
+ : {}),
1774
+ ...(flags['available-tools'] ? { availableTools: String(flags['available-tools']) } : {}),
1775
+ delegate: resolveProtocolTicketDelegate(flags, modelIdentifier, agentIdentifier)
1776
+ };
1777
+
1778
+ const { platformUrl, bearerToken, localSecret, organizationId } =
1779
+ await resolveProtocolAuthForFlags(flags);
1780
+ const timeoutMs = resolveTimeout(flags);
1781
+
1782
+ const data = await apiPost(
1783
+ platformUrl,
1784
+ bearerToken,
1785
+ localSecret,
1786
+ organizationId,
1787
+ '/api/protocol/record-work',
1788
+ body,
1789
+ timeoutMs
1790
+ );
1791
+ console.log(JSON.stringify(data, null, 2));
1792
+
1793
+ const ticketId = data.ticket?.ticket_id ?? data.ticket?.id ?? data.ticket?.ticketId;
1794
+ if (ticketId) {
1795
+ process.stderr.write(`\nTICKET_ID=${ticketId}\n`);
1796
+ }
1797
+ }
1798
+
1657
1799
  // ---------------------------------------------------------------------------
1658
1800
  // search-tickets (find tickets by query/status/project/created_by/dates)
1659
1801
  // ---------------------------------------------------------------------------
@@ -1761,8 +1903,10 @@ Subcommands:
1761
1903
  connect Start a lightweight session without full context
1762
1904
  load-context Read ticket context without creating a session
1763
1905
  search-tickets Find tickets by keyword, status, project, creator, or update date
1906
+ discuss-objective Mark a draft objective as submitted (ready for review/execution)
1764
1907
  create Create a draft ticket without attaching (follow-up or standalone)
1765
1908
  prompt Create a ticket and attach to it immediately
1909
+ record-work Record completed-from-chat work as a ticket in review + feed post (no attach)
1766
1910
  update Post progress, activity events, and optional change rationales
1767
1911
  record-change-rationales Persist structured change rationales without a progress update
1768
1912
  ask Post a blocking question and move the ticket to review
@@ -1998,6 +2142,16 @@ prompt:
1998
2142
  Returns:
1999
2143
  New ticket/session JSON plus SESSION_KEY and TICKET_ID on stderr when available
2000
2144
 
2145
+ discuss-objective:
2146
+ Purpose:
2147
+ Mark the latest draft objective on a ticket as "submitted", indicating the objective
2148
+ has been discussed and is ready for review or execution. Does NOT start execution —
2149
+ use \`attach\` to begin execution.
2150
+ Required:
2151
+ --ticket-id <ticket_id>
2152
+ Optional:
2153
+ --objective-id <id> Target a specific draft objective by UUID
2154
+
2001
2155
  create:
2002
2156
  Purpose:
2003
2157
  Create a draft ticket without attaching to it.
@@ -2024,6 +2178,45 @@ create:
2024
2178
  Notes:
2025
2179
  Standalone create auto-discovers the project from the current working directory unless --personal is set.
2026
2180
  Follow-up create requires both --session-key and --ticket-id.
2181
+ Use create for future work. If the work is already complete and only needs to be recorded,
2182
+ use \`record-work\` instead.
2183
+
2184
+ record-work:
2185
+ Purpose:
2186
+ Record work the agent already completed in a chat as a ticket in \`review\` status
2187
+ with a completed objective, then trigger the feed-post generator. No session is
2188
+ left open — use this INSTEAD OF \`create\` + \`attach\` + \`deliver\` for "log what we
2189
+ just did" flows. Do NOT use this for in-progress work; use \`prompt\` for that.
2190
+ Required:
2191
+ --objective <text> or --objective-file <path|-> What was asked / what was done
2192
+ --summary <text> or --summary-file <path|-> Narrative for feed post + reviewer
2193
+ or: --payload-json <json>
2194
+ or: --payload-file <path|-> containing { objective, summary, artifacts, changeRationales }
2195
+ Optional:
2196
+ --title <text> Auto-derived from objective if omitted
2197
+ --priority <level> low | medium | high | urgent
2198
+ --project-id <id> Skip cwd resolution and use this project explicitly
2199
+ --working-directory <path> Override cwd for project resolution
2200
+ --personal Create a private ticket when no project should be associated
2201
+ --artifacts-json <json>
2202
+ --artifacts-file <path|->
2203
+ --change-rationales-json <json>
2204
+ --change-rationales-file <path|->
2205
+ --skip-file-change-check Bypass local git vs changeRationales validation
2206
+ --acceptance-criteria <text>
2207
+ --available-tools <text>
2208
+ --delegate <model>
2209
+ --agent <identifier>
2210
+ --model <identifier>
2211
+ Notes:
2212
+ Project resolution mirrors \`prompt\`: if --project-id is not set and --personal is not used,
2213
+ the CLI sends cwd as workingDirectory and the API matches it against your configured
2214
+ project_user.local_working_directory rows. If no match, the API returns 400 — re-run with
2215
+ --project-id <id> or --personal.
2216
+ Use this for completed-from-chat work. If you still need to execute the work, use \`create\`
2217
+ for a draft ticket or \`prompt\` to create and start execution immediately.
2218
+ In a git workspace, record-work validates that changed files are represented by
2219
+ changeRationales unless --skip-file-change-check is set.
2027
2220
 
2028
2221
  attachment-list:
2029
2222
  Required:
@@ -2089,6 +2282,8 @@ Examples:
2089
2282
  ovld protocol connect --ticket-id 1:899
2090
2283
  ovld protocol load-context --ticket-id 1:899
2091
2284
  ovld protocol search-tickets --query "auth refactor" --status next-up,execute --limit 10
2285
+ ovld protocol discuss-objective --ticket-id 1:899
2286
+ ovld protocol discuss-objective --ticket-id 1:899 --objective-id <objective-uuid>
2092
2287
  ovld protocol create --agent codex --objective "Capture follow-up work from this repo"
2093
2288
  ovld protocol create --agent codex --session-key <key> --ticket-id <ticket_id> --objective "Capture follow-up work"
2094
2289
  ovld protocol prompt --agent codex --objective "Implement user auth" --priority high
@@ -2110,6 +2305,10 @@ Examples:
2110
2305
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --payload-file -
2111
2306
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --summary "Done" --skip-file-change-check
2112
2307
  ovld protocol deliver --session-key <key> --ticket-id <ticket_id> --summary "Done" --timeout 60000
2308
+ ovld protocol record-work --objective "User asked for X; I did Y" --summary "What I did and why" --change-rationales-json '[...]'
2309
+ ovld protocol record-work --payload-file - <<'EOF'
2310
+ {"objective":"...","summary":"...","artifacts":[...],"changeRationales":[...]}
2311
+ EOF
2113
2312
  `);
2114
2313
  return;
2115
2314
  }
@@ -2138,6 +2337,10 @@ Examples:
2138
2337
  await protocolSearchTickets(args);
2139
2338
  return;
2140
2339
  }
2340
+ if (subcommand === 'discuss-objective') {
2341
+ await protocolDiscussObjective(args);
2342
+ return;
2343
+ }
2141
2344
  if (subcommand === 'create' || subcommand === 'create-ticket') {
2142
2345
  await protocolCreateTicket(args);
2143
2346
  return;
@@ -2146,6 +2349,10 @@ Examples:
2146
2349
  await protocolPrompt(args);
2147
2350
  return;
2148
2351
  }
2352
+ if (subcommand === 'record-work') {
2353
+ await protocolRecordWork(args);
2354
+ return;
2355
+ }
2149
2356
  if (subcommand === 'attachment-list') {
2150
2357
  await protocolAttachmentList(args);
2151
2358
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord-cli",
3
- "version": "0.2605131058.0",
3
+ "version": "0.2605150751.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,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
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",
@@ -9,6 +9,23 @@ BODY=$(cat -)
9
9
  OVERLORD_BASE_URL="${CLAUDE_PLUGIN_OPTION_OVERLORD_URL:-$OVERLORD_URL}"
10
10
  OVERLORD_TOKEN="${CLAUDE_PLUGIN_OPTION_AGENT_TOKEN:-${CLAUDE_PLUGIN_OPTION_OVERLORD_ACCESS_TOKEN:-$OVERLORD_ACCESS_TOKEN}}"
11
11
  OVERLORD_LOCAL_SECRET="${OVERLORD_LOCAL_SECRET:-}"
12
+ HOOK_NAME="claude"
13
+ LOG_DIR="${HOME:-}/.ovld/logs"
14
+ LOG_FILE="${LOG_DIR}/user-prompt-submit-hook.log"
15
+
16
+ log_hook() {
17
+ mkdir -p "$LOG_DIR" 2>/dev/null || return 0
18
+ printf '%s [%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$HOOK_NAME" "$1" >>"$LOG_FILE" 2>/dev/null || true
19
+ }
20
+
21
+ PROMPT_LEN=$(printf '%s' "$BODY" | python3 -c "import json,sys; print(len((json.load(sys.stdin).get('prompt') or '').strip()))" 2>/dev/null || echo "0")
22
+ SESSION_ID=$(printf '%s' "$BODY" | python3 -c "import json,sys; print(json.load(sys.stdin).get('session_id') or 'unknown')" 2>/dev/null || echo "unknown")
23
+ log_hook "received submit session_id=$SESSION_ID prompt_len=$PROMPT_LEN ticket_present=$([ -n "${TICKET_ID:-}" ] && echo yes || echo no)"
24
+
25
+ if [ -z "$OVERLORD_BASE_URL" ] || [ -z "$OVERLORD_TOKEN" ] || [ -z "${TICKET_ID:-}" ]; then
26
+ log_hook "missing required env base_url=$([ -n "$OVERLORD_BASE_URL" ] && echo yes || echo no) token=$([ -n "$OVERLORD_TOKEN" ] && echo yes || echo no) ticket=$([ -n "${TICKET_ID:-}" ] && echo yes || echo no)"
27
+ exit 0
28
+ fi
12
29
 
13
30
  if [ -n "$OVERLORD_BASE_URL" ] && [ -n "$OVERLORD_TOKEN" ] && [ -n "$TICKET_ID" ]; then
14
31
  PAYLOAD=$(
@@ -48,16 +65,31 @@ tid = os.environ.get('TICKET_ID', '')
48
65
  print(json.dumps({'hookType': 'UserPromptSubmit', 'ticketId': tid, 'prompt': text, 'turnIndex': turn_index}))
49
66
  " 2>/dev/null
50
67
  )
51
- if [ -n "$PAYLOAD" ]; then
52
- curl -sf -m 5 \
68
+ if [ -z "$PAYLOAD" ]; then
69
+ log_hook "failed to build hook-event payload from submit body"
70
+ exit 0
71
+ fi
72
+
73
+ PAYLOAD_TURN=$(printf '%s' "$PAYLOAD" | python3 -c "import json,sys; print(json.load(sys.stdin).get('turnIndex', 'unknown'))" 2>/dev/null || echo "unknown")
74
+ log_hook "built payload turn=$PAYLOAD_TURN posting hook-event"
75
+ (
76
+ RESPONSE_FILE=$(mktemp "${TMPDIR:-/tmp}/overlord-hook-event.XXXXXX")
77
+ HTTP_CODE=$(curl -sS -m 5 -o "$RESPONSE_FILE" -w "%{http_code}" \
53
78
  -X POST "$OVERLORD_BASE_URL/api/protocol/hook-event" \
54
79
  -H "Authorization: Bearer $OVERLORD_TOKEN" \
55
80
  -H "X-Overlord-Local-Secret: $OVERLORD_LOCAL_SECRET" \
56
81
  -H "Content-Type: application/json" \
57
- -d "$PAYLOAD" \
58
- >/dev/null 2>&1 &
82
+ -d "$PAYLOAD")
83
+ CURL_EXIT=$?
84
+ RESPONSE_PREVIEW=$(python3 -c "from pathlib import Path; import sys; p=Path(sys.argv[1]); text=p.read_text(encoding='utf-8', errors='replace') if p.exists() else ''; text=' '.join(text.split()); print(text[:200])" "$RESPONSE_FILE" 2>/dev/null || echo '')
85
+ rm -f "$RESPONSE_FILE"
86
+ if [ "$CURL_EXIT" -eq 0 ]; then
87
+ log_hook "hook-event POST finished exit=0 http_status=$HTTP_CODE response_preview=$RESPONSE_PREVIEW"
88
+ else
89
+ log_hook "hook-event POST failed exit=$CURL_EXIT http_status=${HTTP_CODE:-none} response_preview=$RESPONSE_PREVIEW"
90
+ fi
91
+ ) >/dev/null 2>&1 &
59
92
  disown
60
- fi
61
93
  fi
62
94
 
63
95
  exit 0
@@ -84,7 +84,7 @@ Use this mode when the conversation starts normally and the user asks Claude to
84
84
  1. If the user wants to create tickets (and does not ask to start execution), run `ovld protocol create --agent claude-code --objective "..."`.
85
85
  - When `--session-key` and `--ticket-id` are provided, it creates a follow-up draft.
86
86
  - When session flags are omitted, it resolves the project by matching current working directory (or `--working-directory`) to Overlord `local_working_directory`, then creates a standalone draft.
87
- 2. Default to `create` for new tickets. Only use `/overlord:prompt` or `ovld protocol prompt --agent claude-code --objective "..."` when the user explicitly asks to create and execute immediately.
87
+ 2. Default to `create` for new draft tickets. Only use `/overlord:prompt` or `ovld protocol prompt --agent claude-code --objective "..."` when the user explicitly asks to create and execute immediately. If the work is already complete in chat and just needs to be recorded, use `ovld protocol record-work` instead.
88
88
  `prompt` creates the ticket in `execute` status and attaches immediately.
89
89
  3. If the user already has a ticket ID and only wants to inspect it, use `/overlord:load` or run `ovld protocol load-context --ticket-id <ticket_id>`.
90
90
  4. If the user wants to route the current session onto an existing ticket by ID, use `/overlord:connect` or run `ovld protocol connect --ticket-id <ticket_id>`.
@@ -93,6 +93,46 @@ Use this mode when the conversation starts normally and the user asks Claude to
93
93
  7. If you need other lifecycle commands or flags, run `ovld protocol help` and use the real subcommand list instead of guessing.
94
94
  8. Once you attach to a ticket, switch back to Mode 1 and follow the full ticket lifecycle.
95
95
 
96
+ ## Recording Completed Work From Chat
97
+
98
+ Use this when the user has had a working conversation with the agent (no Overlord ticket attached) and now wants to record the outcome as a ticket — both for review and to publish a feed post about it. This is **distinct from `create` and from `deliver`**:
99
+
100
+ - `create` makes a draft ticket for *future* work; nothing happens yet.
101
+ - `prompt` creates a ticket and starts an execution session.
102
+ - `deliver` concludes an *attached* session; the ticket already exists.
103
+ - `record-work` creates the ticket *and* completes the objective *and* triggers the feed-post generator in a single call, with no session left open.
104
+
105
+ Do NOT use `record-work` for in-progress work. Use it only when the work is already done in the chat.
106
+
107
+ ```bash
108
+ ovld protocol record-work \
109
+ --objective "User asked me to X; I did Y by..." \
110
+ --summary "Narrative for the feed post and reviewer." \
111
+ --change-rationales-file -
112
+ ```
113
+
114
+ Or stream the full payload via stdin to avoid quote escaping:
115
+
116
+ ```bash
117
+ ovld protocol record-work --payload-file - <<'EOF'
118
+ {
119
+ "objective": "...",
120
+ "summary": "...",
121
+ "artifacts": [{"type": "next_steps", "label": "...", "content": "..."}],
122
+ "changeRationales": [{"label": "...", "file_path": "...", "summary": "...", "why": "...", "impact": "...", "hunks": [{"header": "@@ ..."}]}]
123
+ }
124
+ EOF
125
+ ```
126
+
127
+ Project resolution mirrors `prompt`/`create`: cwd is matched against the caller's `project_user.local_working_directory`. If no match:
128
+
129
+ 1. **Ask the user for a `--project-id`** if the work is project-related. Show them a brief description of what you're about to record so they can pick the right project.
130
+ 2. If the user confirms the work is not tied to any project, pass `--personal` to create a private ticket.
131
+
132
+ Validation: in a git workspace, `record-work` checks that uncommitted changes are represented by `changeRationales` and rejects if they aren't (same check as `deliver`). Pass `--skip-file-change-check` only when intentionally bypassing.
133
+
134
+ The hosted MCP tool name is `record_work` (snake_case). It accepts the same fields as the CLI but in camelCase JSON. Use `record_work` over the `create_ticket` + `attach` + `deliver` sequence whenever the work is already done.
135
+
96
136
  ## Change Rationales
97
137
 
98
138
  Always include `changeRationales` when delivering. Optionally include them on updates during long-running work.
@@ -160,10 +200,20 @@ ovld protocol attachment-download-url --session-key <sessionKey> --ticket-id $TI
160
200
 
161
201
  The `attach` and `load-context` responses already include `attachments` and `objectives` arrays — use those for `<attachment-id>` and `<objective-id>` values. Run `attachment-list` mid-session if new files have been uploaded since attach.
162
202
 
203
+ ## Large Artifacts
204
+
205
+ For large artifacts such as planning documents, architecture decisions, research summaries, or design documents: **save the full content as a markdown file in the linked repository, then summarize it in the artifact returned to the ticket.**
206
+
207
+ - Save to a meaningful path in the repository (e.g., `ai/feature-plans/my-feature.md` for feature plans, `docs/decisions/my-decision.md` for architecture decision records).
208
+ - Commit the file as part of the ticket's work so it appears in `changeRationales`.
209
+ - In the delivery, include a `note` or `decision` artifact with a concise summary and the repository file path — not the full document content.
210
+
211
+ This keeps the ticket feed readable while preserving the full document in version control where it can be reviewed, diffed, and referenced later.
212
+
163
213
  ## Defaults And Notes
164
214
 
165
215
  - API requires `agentIdentifier` and `connectionMethod` on attach/connect/prompt. The CLI defaults them to `claude-code`/`cli`; the MCP tool defaults to `mcp`. Override with `--agent` / `--method` when calling from a different runtime.
166
- - Hosted Overlord MCP (`/functions/v1/mcp`) uses the same canonical tool names as any local MCP shim that shells into `ovld protocol` (`attach`, `update`, `deliver`, …). Hosted calls use camelCase JSON keys (`ticketId`, `sessionKey`) matching `POST /api/protocol/*` bodies; the local shim uses snake_case keys mapped to CLI flags (`ticket_id`, `session_key`).
216
+ - Hosted Overlord MCP (`/functions/v1/mcp`) uses the same canonical tool names as any local MCP shim that shells into `ovld protocol` (`attach`, `update`, `deliver`, `record_work`, …). Hosted calls use camelCase JSON keys (`ticketId`, `sessionKey`) matching `POST /api/protocol/*` bodies; the local shim uses snake_case keys mapped to CLI flags (`ticket_id`, `session_key`).
167
217
  - `permission-request` is invoked by the Claude Code permission hook installed by the bundle. Agents do not normally call it directly.
168
218
  - `record_change_rationales` (MCP) and `ovld protocol record-change-rationales` (CLI) both write to the same `file_changes` table. The dedicated CLI route is `POST /api/protocol/record-change-rationales`.
169
219
  - Objective attachment tools follow the `<verb>_<noun>` MCP naming: `list_attachments`, `prepare_attachment_upload`, `finalize_attachment_upload`, `get_attachment_download_url`, `upload_attachment_file`. CLI commands use `attachment-*` and require `--objective-id` for upload/finalize.
@@ -182,4 +232,4 @@ The `attach` and `load-context` responses already include `attachments` and `obj
182
232
  - Do not add or commit changes unless the user explicitly asks you to commit.
183
233
  - Delivery is the concluding step. After delivering, stop unless the user follows up or the ticket is reopened.
184
234
 
185
- <!-- version: 0.4.9 -->
235
+ <!-- version: 0.4.10 -->
@@ -68,7 +68,7 @@ ovld protocol deliver --session-key <sessionKey> \
68
68
  1. If the user wants to create tickets (and does not ask to start execution), use `/create` or run `ovld protocol create --agent cursor --objective "..."`.
69
69
  - When `--session-key` and `--ticket-id` are provided, it creates a follow-up draft.
70
70
  - When session flags are omitted, it resolves the project by matching current working directory (or `--working-directory`) to Overlord `local_working_directory`, then creates a standalone draft.
71
- 2. Default to `create` for new tickets. Only use `/prompt` or `ovld protocol prompt --agent cursor --objective "..."` when the user explicitly asks to create and execute immediately.
71
+ 2. Default to `create` for new draft tickets. Only use `/prompt` or `ovld protocol prompt --agent cursor --objective "..."` when the user explicitly asks to create and execute immediately. If the work is already complete in chat and just needs to be recorded, use `ovld protocol record-work` instead.
72
72
  3. If the user already has a ticket ID and only wants to inspect it, use `/load` or run `ovld protocol load-context --ticket-id <ticket_id>`.
73
73
  4. If the user wants to route the current session onto an existing ticket by ID, use `/connect` or run `ovld protocol connect --ticket-id <ticket_id>`.
74
74
  5. If the user wants to establish a persistent session with a ticket by ID, use `/attach` or run `ovld protocol attach --ticket-id <ticket_id>`.
@@ -76,6 +76,10 @@ ovld protocol deliver --session-key <sessionKey> \
76
76
  7. If you need other lifecycle commands or flags, run `ovld protocol help` and use the real subcommand list instead of guessing.
77
77
  8. Once you attach to a ticket, switch back to Mode 1 and follow the full ticket lifecycle.
78
78
 
79
+ ## Recording Completed Work From Chat
80
+
81
+ Use `ovld protocol record-work` when the user has done work directly in chat (no attached ticket) and wants to record it as a ticket plus publish a feed post. This is distinct from `create` (drafts future work), `prompt` (creates + executes), and `deliver` (concludes an attached session). `record-work` creates a ticket in `review` with a completed objective and triggers the feed-post generator atomically. Project resolution mirrors `prompt`: cwd is matched against `project_user.local_working_directory`; if no match, ask the user for `--project-id` or pass `--personal`. Do NOT use for in-progress work.
82
+
79
83
  ## Change Rationales
80
84
 
81
85
  Always include `changeRationales` when delivering. Optionally include them on updates during long-running work.
@@ -134,10 +138,20 @@ Objective attachment uploads also expose two-step variants — `attachment-prepa
134
138
 
135
139
  "Artifacts" in `deliver` are the structured records an agent submits at delivery time (next_steps, test_results, migration, decision, note, url) — not user-uploaded files.
136
140
 
141
+ ## Large Artifacts
142
+
143
+ For large artifacts such as planning documents, architecture decisions, research summaries, or design documents: **save the full content as a markdown file in the linked repository, then summarize it in the artifact returned to the ticket.**
144
+
145
+ - Save to a meaningful path in the repository (e.g., `ai/feature-plans/my-feature.md` for feature plans, `docs/decisions/my-decision.md` for architecture decision records).
146
+ - Commit the file as part of the ticket's work so it appears in `changeRationales`.
147
+ - In the delivery, include a `note` or `decision` artifact with a concise summary and the repository file path — not the full document content.
148
+
149
+ This keeps the ticket feed readable while preserving the full document in version control where it can be reviewed, diffed, and referenced later.
150
+
137
151
  ## Defaults And Notes
138
152
 
139
153
  - The Overlord API requires `agentIdentifier` and `connectionMethod` on attach/connect/prompt, but the CLI defaults them to `cursor`/`cli` (override with `--agent` / `--method`). The MCP tools default to `mcp`.
140
- - The Cursor local MCP stub exposes `attach`, `update`, and `deliver` only (same names as the hosted Overlord MCP core set).
154
+ - The Cursor plugin ships the shared local `overlord-mcp.mjs` shim. Treat its tool list as the source of truth; it includes `record_work` alongside the core lifecycle tools.
141
155
  - `permission-request` is invoked by the installed permission hook; agents normally do not call it directly.
142
156
  - The `record_change_rationales` MCP tool and `ovld protocol record-change-rationales` CLI both write to the same `file_changes` table; pick whichever fits your runtime.
143
157
 
@@ -152,4 +166,4 @@ Objective attachment uploads also expose two-step variants — `attachment-prepa
152
166
  - 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
167
  - Delivery is the concluding step. After delivering, stop unless the user follows up or the ticket is reopened.
154
168
 
155
- <!-- version: 0.4.9 -->
169
+ <!-- version: 0.4.10 -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overlord",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Chat-driven access to Overlord ticket protocol workflows with hook-driven activity capture.",
5
5
  "author": {
6
6
  "name": "Cooperativ",
@@ -393,6 +393,58 @@ const tools = [
393
393
  }),
394
394
  subcommand: 'deliver'
395
395
  },
396
+ {
397
+ name: 'record_work',
398
+ description:
399
+ 'Record completed-from-chat work as a ticket in `review` status with a completed objective, then trigger feed-post generation. Use this INSTEAD OF create_ticket + attach + deliver for "log what we just did" flows. Project resolution mirrors `prompt`: if project_id is omitted, working_directory is matched against project_user.local_working_directory. When neither resolves, pass personal: true. Do not use for in-progress work.',
400
+ inputSchema: {
401
+ type: 'object',
402
+ properties: {
403
+ objective: {
404
+ type: 'string',
405
+ description: 'What was asked / what was done. Stored as a completed objective.'
406
+ },
407
+ summary: {
408
+ type: 'string',
409
+ description: 'Narrative for feed post + reviewer.'
410
+ },
411
+ title: { type: 'string' },
412
+ priority: { type: 'string', enum: ['low', 'medium', 'high', 'urgent'] },
413
+ project_id: { type: 'string' },
414
+ working_directory: { type: 'string' },
415
+ personal: { type: 'boolean' },
416
+ acceptance_criteria: { type: 'string' },
417
+ available_tools: { type: 'string' },
418
+ delegate: { type: 'string' },
419
+ artifacts: { type: 'array' },
420
+ change_rationales: { type: 'array' },
421
+ skip_file_change_check: { type: 'boolean' }
422
+ },
423
+ required: ['objective', 'summary']
424
+ },
425
+ toCliFlags: args => ({
426
+ 'payload-file': '-',
427
+ title: args.title,
428
+ priority: args.priority,
429
+ 'project-id': args.project_id,
430
+ 'working-directory': args.working_directory,
431
+ personal: args.personal,
432
+ 'acceptance-criteria': args.acceptance_criteria,
433
+ 'available-tools': args.available_tools,
434
+ delegate: args.delegate,
435
+ 'skip-file-change-check': args.skip_file_change_check
436
+ }),
437
+ toCliStdin: args =>
438
+ JSON.stringify({
439
+ objective: args.objective,
440
+ summary: args.summary,
441
+ ...(Array.isArray(args.artifacts) ? { artifacts: args.artifacts } : {}),
442
+ ...(Array.isArray(args.change_rationales)
443
+ ? { changeRationales: args.change_rationales }
444
+ : {})
445
+ }),
446
+ subcommand: 'record-work'
447
+ },
396
448
  {
397
449
  name: 'list_attachments',
398
450
  description:
@@ -5,24 +5,55 @@ BODY=$(cat -)
5
5
  OVERLORD_BASE_URL="${OVERLORD_URL:-}"
6
6
  OVERLORD_TOKEN="${OVERLORD_ACCESS_TOKEN:-}"
7
7
  OVERLORD_LOCAL_SECRET="${OVERLORD_LOCAL_SECRET:-}"
8
+ HOOK_NAME="codex"
9
+ LOG_DIR="${HOME:-}/.ovld/logs"
10
+ LOG_FILE="${LOG_DIR}/user-prompt-submit-hook.log"
11
+
12
+ log_hook() {
13
+ mkdir -p "$LOG_DIR" 2>/dev/null || return 0
14
+ printf '%s [%s] %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$HOOK_NAME" "$1" >>"$LOG_FILE" 2>/dev/null || true
15
+ }
8
16
 
9
17
  TURN=$(printf '%s' "$BODY" | python3 -c "import json,sys; print(json.load(sys.stdin).get('turn_number', 0))" 2>/dev/null || echo "0")
18
+ PROMPT_LEN=$(printf '%s' "$BODY" | python3 -c "import json,sys; print(len((json.load(sys.stdin).get('prompt') or '').strip()))" 2>/dev/null || echo "0")
19
+ log_hook "received submit turn=$TURN prompt_len=$PROMPT_LEN ticket_present=$([ -n "${TICKET_ID:-}" ] && echo yes || echo no)"
10
20
  if [ "$TURN" = "0" ]; then
21
+ log_hook "skipping initial submit turn=0"
22
+ exit 0
23
+ fi
24
+
25
+ if [ -z "$OVERLORD_BASE_URL" ] || [ -z "$OVERLORD_TOKEN" ] || [ -z "${TICKET_ID:-}" ]; then
26
+ log_hook "missing required env base_url=$([ -n "$OVERLORD_BASE_URL" ] && echo yes || echo no) token=$([ -n "$OVERLORD_TOKEN" ] && echo yes || echo no) ticket=$([ -n "${TICKET_ID:-}" ] && echo yes || echo no)"
11
27
  exit 0
12
28
  fi
13
29
 
14
30
  if [ -n "$OVERLORD_BASE_URL" ] && [ -n "$OVERLORD_TOKEN" ] && [ -n "$TICKET_ID" ]; then
15
31
  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 \
32
+ if [ -z "$PAYLOAD" ]; then
33
+ log_hook "failed to build hook-event payload from submit body"
34
+ exit 0
35
+ fi
36
+
37
+ PAYLOAD_TURN=$(printf '%s' "$PAYLOAD" | python3 -c "import json,sys; print(json.load(sys.stdin).get('turnIndex', 'unknown'))" 2>/dev/null || echo "unknown")
38
+ log_hook "built payload turn=$PAYLOAD_TURN posting hook-event"
39
+ (
40
+ RESPONSE_FILE=$(mktemp "${TMPDIR:-/tmp}/overlord-hook-event.XXXXXX")
41
+ HTTP_CODE=$(curl -sS -m 5 -o "$RESPONSE_FILE" -w "%{http_code}" \
18
42
  -X POST "$OVERLORD_BASE_URL/api/protocol/hook-event" \
19
43
  -H "Authorization: Bearer $OVERLORD_TOKEN" \
20
44
  -H "X-Overlord-Local-Secret: $OVERLORD_LOCAL_SECRET" \
21
45
  -H "Content-Type: application/json" \
22
- -d "$PAYLOAD" \
23
- >/dev/null 2>&1 &
46
+ -d "$PAYLOAD")
47
+ CURL_EXIT=$?
48
+ RESPONSE_PREVIEW=$(python3 -c "from pathlib import Path; import sys; p=Path(sys.argv[1]); text=p.read_text(encoding='utf-8', errors='replace') if p.exists() else ''; text=' '.join(text.split()); print(text[:200])" "$RESPONSE_FILE" 2>/dev/null || echo '')
49
+ rm -f "$RESPONSE_FILE"
50
+ if [ "$CURL_EXIT" -eq 0 ]; then
51
+ log_hook "hook-event POST finished exit=0 http_status=$HTTP_CODE response_preview=$RESPONSE_PREVIEW"
52
+ else
53
+ log_hook "hook-event POST failed exit=$CURL_EXIT http_status=${HTTP_CODE:-none} response_preview=$RESPONSE_PREVIEW"
54
+ fi
55
+ ) >/dev/null 2>&1 &
24
56
  disown
25
- fi
26
57
  fi
27
58
 
28
59
  exit 0
@@ -72,7 +72,7 @@ ovld protocol deliver --session-key <sessionKey> \
72
72
  1. If the user wants to create tickets (and does not ask to start execution), run `ovld protocol create --agent codex --objective "..."`.
73
73
  - When `--session-key` and `--ticket-id` are provided, it creates a follow-up draft.
74
74
  - When session flags are omitted, it resolves the project by matching current working directory (or `--working-directory`) to Overlord `local_working_directory`, then creates a standalone draft.
75
- 2. Default to `create` for new tickets. Only use `ovld protocol prompt --agent codex --objective "..."` when the user explicitly asks to create and execute immediately.
75
+ 2. Default to `create` for new draft tickets. Only use `ovld protocol prompt --agent codex --objective "..."` when the user explicitly asks to create and execute immediately. If the work is already complete in chat and just needs to be recorded, use `ovld protocol record-work` instead.
76
76
  `prompt` creates the ticket in `execute` status and attaches immediately.
77
77
  3. If the user wants to inspect an existing ticket without starting work, use `ovld protocol load-context --ticket-id <ticket_id>`.
78
78
  4. If the user wants to work an existing ticket, attach with `ovld protocol attach --ticket-id <ticket_id>` and then switch to Mode 1. Use `ovld protocol connect --ticket-id <ticket_id>` instead when you only need a session key without the full ticket payload.
@@ -105,6 +105,23 @@ Pass `--execution-target agent` or `--execution-target human` (default: `human`)
105
105
 
106
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`.
107
107
 
108
+ ## Recording Completed Work From Chat
109
+
110
+ Use `ovld protocol record-work` (hosted MCP: `record_work`) when the user has done work directly in a chat (no attached ticket) and now wants to record it as a ticket *and* publish a feed post about it. Distinct from `create` (drafts future work), `prompt` (creates and starts execution), and `deliver` (concludes an attached session). `record-work` creates the ticket in `review` with a completed objective and triggers the feed-post generator atomically; no session is left open.
111
+
112
+ ```bash
113
+ ovld protocol record-work --payload-file - <<'EOF'
114
+ {
115
+ "objective": "User asked me to X; I did Y...",
116
+ "summary": "Narrative for feed post + reviewer.",
117
+ "artifacts": [...],
118
+ "changeRationales": [...]
119
+ }
120
+ EOF
121
+ ```
122
+
123
+ Project resolution mirrors `prompt`/`create`: cwd is matched against `project_user.local_working_directory`. If no match, ask the user for `--project-id`; if the work is not tied to any project, pass `--personal`. In a git workspace, `record-work` validates uncommitted changes are represented by `changeRationales` unless `--skip-file-change-check` is set. Do NOT use for in-progress work.
124
+
108
125
  ## Change Rationales
109
126
 
110
127
  Always include `changeRationales` when delivering. Optionally include them on updates during long-running work.
@@ -139,10 +156,20 @@ ovld protocol attachment-download-url --session-key <sessionKey> --ticket-id $TI
139
156
 
140
157
  The `attach` and `load-context` responses already include `attachments` and `objectives` arrays — use those for `<attachment-id>` and `<objective-id>` values. Run `attachment-list` mid-session if new files have been uploaded since attach.
141
158
 
159
+ ## Large Artifacts
160
+
161
+ For large artifacts such as planning documents, architecture decisions, research summaries, or design documents: **save the full content as a markdown file in the linked repository, then summarize it in the artifact returned to the ticket.**
162
+
163
+ - Save to a meaningful path in the repository (e.g., `ai/feature-plans/my-feature.md` for feature plans, `docs/decisions/my-decision.md` for architecture decision records).
164
+ - Commit the file as part of the ticket's work so it appears in `changeRationales`.
165
+ - In the delivery, include a `note` or `decision` artifact with a concise summary and the repository file path — not the full document content.
166
+
167
+ This keeps the ticket feed readable while preserving the full document in version control where it can be reviewed, diffed, and referenced later.
168
+
142
169
  ## Defaults And Notes
143
170
 
144
171
  - API requires `agentIdentifier` and `connectionMethod` on attach/connect/prompt. The CLI defaults them to `codex`/`cli`; the hosted MCP defaults to `mcp`. Override with `--agent` / `--method` when calling from a different runtime.
145
- - Hosted MCP and the local `overlord-mcp.mjs` shim use the same **tool names** (\`attach\`, \`update\`, \`deliver\`, …). The shim uses **snake_case** JSON keys (\`ticket_id\`, \`session_key\`) that map to CLI flags; hosted MCP uses **camelCase** (\`ticketId\`, \`sessionKey\`) matching REST bodies.
172
+ - Hosted MCP and the local `overlord-mcp.mjs` shim use the same **tool names** (\`attach\`, \`update\`, \`deliver\`, \`record_work\`, …). The shim uses **snake_case** JSON keys (\`ticket_id\`, \`session_key\`) that map to CLI flags; hosted MCP uses **camelCase** (\`ticketId\`, \`sessionKey\`) matching REST bodies.
146
173
  - `permission-request` is invoked by the local Codex plugin's permission rules; agents do not normally call it directly.
147
174
  - `record_change_rationales` (MCP) and `ovld protocol record-change-rationales` (CLI) both write to the `file_changes` table; the dedicated route is `POST /api/protocol/record-change-rationales`.
148
175
  - Objective attachment MCP tools use `<verb>_<noun>` names — `list_attachments`, `prepare_attachment_upload`, `finalize_attachment_upload`, `get_attachment_download_url`, `upload_attachment_file`. CLI commands use `attachment-*` and require `--objective-id` for upload/finalize.
@@ -158,4 +185,4 @@ The `attach` and `load-context` responses already include `attachments` and `obj
158
185
  - 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.
159
186
  - 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.
160
187
 
161
- <!-- version: 0.4.9 -->
188
+ <!-- version: 0.4.10 -->