@penclipai/adapter-codex-local 2026.531.0 → 2026.602.0-canary.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@penclipai/adapter-codex-local",
3
- "version": "2026.531.0",
3
+ "version": "2026.602.0-canary.0",
4
4
  "license": "MIT",
5
5
  "homepage": "https://github.com/penclipai/paperclip-cn",
6
6
  "bugs": {
@@ -38,7 +38,7 @@
38
38
  "skills"
39
39
  ],
40
40
  "dependencies": {
41
- "@penclipai/adapter-utils": "2026.531.0",
41
+ "@penclipai/adapter-utils": "2026.602.0-canary.0",
42
42
  "picocolors": "^1.1.1"
43
43
  },
44
44
  "devDependencies": {
@@ -94,6 +94,12 @@ If `currentParticipant` does not match you, do not try to advance the stage —
94
94
  - If blocked, move the issue to `blocked` with the unblock owner and exact action needed.
95
95
  - Respect budget, pause/cancel, approval gates, execution policy stages, and company boundaries.
96
96
 
97
+ ### Generated Artifacts and Work Products
98
+
99
+ When work produces a user-inspectable file, upload it to the current issue before final disposition. Local filesystem paths are not enough because board users, reviewers, and cloud operators may not have access to the agent workspace.
100
+
101
+ For technical upload instructions, read `references/artifacts.md`.
102
+
97
103
  **Step 8 — Update status and communicate.** Always include the run ID header.
98
104
  If you are blocked at any point, you MUST update the issue to `blocked` before exiting the heartbeat, with a comment that explains the blocker and who needs to act.
99
105
 
@@ -0,0 +1,44 @@
1
+ # Generated Artifacts and Work Products
2
+
3
+ When work produces a user-inspectable file, upload it to the current issue before final disposition. Local filesystem paths are not enough because board users, reviewers, and cloud operators may not have access to the agent workspace.
4
+
5
+ Use the helper bundled with this skill. From an installed `paperclip` skill directory, the helper lives at `scripts/paperclip-upload-artifact.sh`:
6
+
7
+ ```bash
8
+ scripts/paperclip-upload-artifact.sh path/to/output.webm \
9
+ --title "Walkthrough render" \
10
+ --summary "Rendered walkthrough for review"
11
+ ```
12
+
13
+ The helper uses `PAPERCLIP_API_URL`, `PAPERCLIP_API_KEY`, `PAPERCLIP_COMPANY_ID`, `PAPERCLIP_TASK_ID`, and `PAPERCLIP_RUN_ID`. It uploads the file as an issue attachment, creates an attachment-backed artifact work product by default, and prints issue-safe markdown links for your final comment.
14
+
15
+ If the helper is unavailable, use the Paperclip API directly:
16
+
17
+ ```bash
18
+ curl -sS -X POST \
19
+ "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/issues/$PAPERCLIP_TASK_ID/attachments" \
20
+ -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
21
+ -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
22
+ -F 'file=@"path/to/output.webm";type=video/webm'
23
+ ```
24
+
25
+ Then create a work product when the file is the deliverable. The server canonicalizes attachment-backed artifact metadata from the `attachmentId`:
26
+
27
+ ```bash
28
+ curl -sS -X POST \
29
+ "$PAPERCLIP_API_URL/api/issues/$PAPERCLIP_TASK_ID/work-products" \
30
+ -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
31
+ -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
32
+ -H "Content-Type: application/json" \
33
+ --data-binary '{
34
+ "type": "artifact",
35
+ "provider": "paperclip",
36
+ "title": "Walkthrough render",
37
+ "status": "ready_for_review",
38
+ "reviewState": "needs_board_review",
39
+ "isPrimary": true,
40
+ "metadata": { "attachmentId": "<uploaded-attachment-id>" }
41
+ }'
42
+ ```
43
+
44
+ In your final issue comment, link the uploaded attachment or work product and describe what it contains. Do not leave artifact-producing work `in_progress` with only a local path or a `Remaining` note.
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ usage() {
6
+ cat <<'EOF'
7
+ Usage:
8
+ paperclip-upload-artifact.sh FILE [options]
9
+
10
+ Uploads a generated file from the current workspace to the current Paperclip
11
+ issue, then creates an attachment-backed artifact work product by default.
12
+
13
+ Required environment for live uploads:
14
+ PAPERCLIP_API_URL, PAPERCLIP_API_KEY, PAPERCLIP_COMPANY_ID, PAPERCLIP_TASK_ID, PAPERCLIP_RUN_ID
15
+
16
+ Options:
17
+ --issue-id ID Issue id to attach to (default: PAPERCLIP_TASK_ID)
18
+ --company-id ID Company id (default: PAPERCLIP_COMPANY_ID)
19
+ --title TEXT Work product title (default: file basename)
20
+ --summary TEXT Work product summary
21
+ --content-type TYPE Override detected upload content type
22
+ --status STATUS Work product status (default: ready_for_review)
23
+ --no-work-product Only upload the issue attachment
24
+ --no-primary Do not mark the artifact work product primary for its type
25
+ --output FORMAT markdown or json (default: markdown)
26
+ --dry-run Print resolved upload settings without calling the API
27
+ --help, -h Show this help
28
+
29
+ Examples:
30
+ scripts/paperclip-upload-artifact.sh dist/demo.mp4 \
31
+ --title "Demo video render" \
32
+ --summary "MP4 render for board review"
33
+
34
+ scripts/paperclip-upload-artifact.sh out/walkthrough.webm \
35
+ --title "Walkthrough video" \
36
+ --content-type video/webm
37
+ EOF
38
+ }
39
+
40
+ require_command() {
41
+ if ! command -v "$1" >/dev/null 2>&1; then
42
+ printf 'Missing required command: %s\n' "$1" >&2
43
+ exit 1
44
+ fi
45
+ }
46
+
47
+ json_bool() {
48
+ if [[ "${1:-0}" == "1" ]]; then
49
+ printf 'true'
50
+ else
51
+ printf 'false'
52
+ fi
53
+ }
54
+
55
+ detect_content_type() {
56
+ local path="$1"
57
+ local lower
58
+ lower="$(printf '%s' "$path" | tr '[:upper:]' '[:lower:]')"
59
+
60
+ case "$lower" in
61
+ *.mp4|*.m4v) printf 'video/mp4' ;;
62
+ *.webm) printf 'video/webm' ;;
63
+ *.mov|*.qt) printf 'video/quicktime' ;;
64
+ *.png) printf 'image/png' ;;
65
+ *.jpg|*.jpeg) printf 'image/jpeg' ;;
66
+ *.gif) printf 'image/gif' ;;
67
+ *.webp) printf 'image/webp' ;;
68
+ *.svg) printf 'image/svg+xml' ;;
69
+ *.pdf) printf 'application/pdf' ;;
70
+ *.txt|*.log) printf 'text/plain' ;;
71
+ *.md|*.markdown) printf 'text/markdown' ;;
72
+ *.json) printf 'application/json' ;;
73
+ *.csv) printf 'text/csv' ;;
74
+ *.html|*.htm) printf 'text/html' ;;
75
+ *.zip) printf 'application/zip' ;;
76
+ *)
77
+ if command -v file >/dev/null 2>&1; then
78
+ file --brief --mime-type "$path"
79
+ else
80
+ printf 'application/octet-stream'
81
+ fi
82
+ ;;
83
+ esac
84
+ }
85
+
86
+ request_json() {
87
+ local method="$1"
88
+ local url="$2"
89
+ local body="${3:-}"
90
+ local response_file
91
+ local status_code
92
+
93
+ response_file="$(mktemp)"
94
+ if [[ -n "$body" ]]; then
95
+ status_code="$(
96
+ curl -sS -X "$method" -w '%{http_code}' -o "$response_file" \
97
+ "$url" \
98
+ -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
99
+ -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
100
+ -H 'Content-Type: application/json' \
101
+ --data-binary "$body"
102
+ )"
103
+ else
104
+ status_code="$(
105
+ curl -sS -X "$method" -w '%{http_code}' -o "$response_file" \
106
+ "$url" \
107
+ -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
108
+ -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID"
109
+ )"
110
+ fi
111
+
112
+ if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then
113
+ printf 'Request failed (%s): %s\n' "$status_code" "$url" >&2
114
+ cat "$response_file" >&2
115
+ printf '\n' >&2
116
+ rm -f "$response_file"
117
+ exit 1
118
+ fi
119
+
120
+ cat "$response_file"
121
+ rm -f "$response_file"
122
+ }
123
+
124
+ upload_file() {
125
+ local url="$1"
126
+ local path="$2"
127
+ local content_type="$3"
128
+ local escaped_path
129
+ local response_file
130
+ local status_code
131
+
132
+ escaped_path="${path//\\/\\\\}"
133
+ escaped_path="${escaped_path//\"/\\\"}"
134
+ response_file="$(mktemp)"
135
+ status_code="$(
136
+ curl -sS -X POST -w '%{http_code}' -o "$response_file" \
137
+ "$url" \
138
+ -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
139
+ -H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID" \
140
+ -F "file=@\"${escaped_path}\";type=${content_type}"
141
+ )"
142
+
143
+ if [[ "$status_code" -lt 200 || "$status_code" -ge 300 ]]; then
144
+ printf 'Upload failed (%s): %s\n' "$status_code" "$url" >&2
145
+ cat "$response_file" >&2
146
+ printf '\n' >&2
147
+ rm -f "$response_file"
148
+ exit 1
149
+ fi
150
+
151
+ cat "$response_file"
152
+ rm -f "$response_file"
153
+ }
154
+
155
+ file_path=""
156
+ issue_id="${PAPERCLIP_TASK_ID:-}"
157
+ company_id="${PAPERCLIP_COMPANY_ID:-}"
158
+ title=""
159
+ summary=""
160
+ content_type=""
161
+ status="ready_for_review"
162
+ create_work_product=1
163
+ is_primary=1
164
+ output_format="markdown"
165
+ dry_run=0
166
+
167
+ while [[ $# -gt 0 ]]; do
168
+ case "$1" in
169
+ --issue-id)
170
+ issue_id="${2:-}"
171
+ shift 2
172
+ ;;
173
+ --company-id)
174
+ company_id="${2:-}"
175
+ shift 2
176
+ ;;
177
+ --title)
178
+ title="${2:-}"
179
+ shift 2
180
+ ;;
181
+ --summary)
182
+ summary="${2:-}"
183
+ shift 2
184
+ ;;
185
+ --content-type)
186
+ content_type="${2:-}"
187
+ shift 2
188
+ ;;
189
+ --status)
190
+ status="${2:-}"
191
+ shift 2
192
+ ;;
193
+ --no-work-product)
194
+ create_work_product=0
195
+ shift
196
+ ;;
197
+ --no-primary)
198
+ is_primary=0
199
+ shift
200
+ ;;
201
+ --output)
202
+ output_format="${2:-}"
203
+ shift 2
204
+ ;;
205
+ --dry-run)
206
+ dry_run=1
207
+ shift
208
+ ;;
209
+ --help|-h)
210
+ usage
211
+ exit 0
212
+ ;;
213
+ --*)
214
+ printf 'Unknown argument: %s\n' "$1" >&2
215
+ usage >&2
216
+ exit 1
217
+ ;;
218
+ *)
219
+ if [[ -n "$file_path" ]]; then
220
+ printf 'Unexpected positional argument: %s\n' "$1" >&2
221
+ usage >&2
222
+ exit 1
223
+ fi
224
+ file_path="$1"
225
+ shift
226
+ ;;
227
+ esac
228
+ done
229
+
230
+ if [[ -z "$file_path" ]]; then
231
+ printf 'Missing file path.\n' >&2
232
+ usage >&2
233
+ exit 1
234
+ fi
235
+
236
+ if [[ ! -f "$file_path" ]]; then
237
+ printf 'Artifact file does not exist: %s\n' "$file_path" >&2
238
+ exit 1
239
+ fi
240
+
241
+ if [[ "$output_format" != "markdown" && "$output_format" != "json" ]]; then
242
+ printf 'Unsupported output format: %s\n' "$output_format" >&2
243
+ exit 1
244
+ fi
245
+
246
+ require_command curl
247
+ require_command jq
248
+
249
+ if [[ -z "$title" ]]; then
250
+ title="$(basename "$file_path")"
251
+ fi
252
+
253
+ if [[ -z "$content_type" ]]; then
254
+ content_type="$(detect_content_type "$file_path")"
255
+ fi
256
+
257
+ if [[ "$dry_run" == "1" ]]; then
258
+ create_work_product_json="$(json_bool "$create_work_product")"
259
+ is_primary_json="$(json_bool "$is_primary")"
260
+ jq -n \
261
+ --arg file "$file_path" \
262
+ --arg issueId "$issue_id" \
263
+ --arg companyId "$company_id" \
264
+ --arg title "$title" \
265
+ --arg summary "$summary" \
266
+ --arg contentType "$content_type" \
267
+ --arg status "$status" \
268
+ --argjson createWorkProduct "$create_work_product_json" \
269
+ --argjson isPrimary "$is_primary_json" \
270
+ '{file: $file, issueId: $issueId, companyId: $companyId, title: $title, summary: $summary, contentType: $contentType, status: $status, createWorkProduct: $createWorkProduct, isPrimary: $isPrimary}'
271
+ exit 0
272
+ fi
273
+
274
+ if [[ -z "${PAPERCLIP_API_URL:-}" || -z "${PAPERCLIP_API_KEY:-}" || -z "${PAPERCLIP_RUN_ID:-}" ]]; then
275
+ printf 'Missing PAPERCLIP_API_URL, PAPERCLIP_API_KEY, or PAPERCLIP_RUN_ID.\n' >&2
276
+ exit 1
277
+ fi
278
+
279
+ if [[ -z "$issue_id" || -z "$company_id" ]]; then
280
+ printf 'Missing issue or company id. Pass --issue-id/--company-id or set PAPERCLIP_TASK_ID/PAPERCLIP_COMPANY_ID.\n' >&2
281
+ exit 1
282
+ fi
283
+
284
+ api_base="${PAPERCLIP_API_URL%/}/api"
285
+ attachment="$(
286
+ upload_file \
287
+ "$api_base/companies/$company_id/issues/$issue_id/attachments" \
288
+ "$file_path" \
289
+ "$content_type"
290
+ )"
291
+
292
+ work_product="null"
293
+ if [[ "$create_work_product" == "1" ]]; then
294
+ is_primary_json="$(json_bool "$is_primary")"
295
+ attachment_id="$(jq -r '.id // empty' <<<"$attachment")"
296
+ byte_size="$(jq -r '.byteSize // 0' <<<"$attachment")"
297
+ content_path="$(jq -r '.contentPath // empty' <<<"$attachment")"
298
+ open_path="$(jq -r '.openPath // .contentPath // empty' <<<"$attachment")"
299
+ download_path="$(jq -r '.downloadPath // (if .contentPath then (.contentPath + "?download=1") else "" end)' <<<"$attachment")"
300
+ original_filename="$(jq -r '.originalFilename // empty' <<<"$attachment")"
301
+
302
+ if [[ -z "$attachment_id" || -z "$content_path" || -z "$download_path" ]]; then
303
+ printf 'Upload response did not include attachment path metadata.\n' >&2
304
+ printf '%s\n' "$attachment" >&2
305
+ exit 1
306
+ fi
307
+
308
+ work_product_payload="$(
309
+ jq -nc \
310
+ --arg title "$title" \
311
+ --arg summary "$summary" \
312
+ --arg status "$status" \
313
+ --arg runId "$PAPERCLIP_RUN_ID" \
314
+ --arg attachmentId "$attachment_id" \
315
+ --arg contentType "$content_type" \
316
+ --argjson byteSize "$byte_size" \
317
+ --arg contentPath "$content_path" \
318
+ --arg openPath "$open_path" \
319
+ --arg downloadPath "$download_path" \
320
+ --arg originalFilename "$original_filename" \
321
+ --argjson isPrimary "$is_primary_json" \
322
+ '{
323
+ type: "artifact",
324
+ provider: "paperclip",
325
+ title: $title,
326
+ status: $status,
327
+ reviewState: "none",
328
+ isPrimary: $isPrimary,
329
+ healthStatus: "unknown",
330
+ summary: (if $summary == "" then null else $summary end),
331
+ createdByRunId: $runId,
332
+ metadata: {
333
+ attachmentId: $attachmentId,
334
+ contentType: $contentType,
335
+ byteSize: $byteSize,
336
+ contentPath: $contentPath,
337
+ openPath: $openPath,
338
+ downloadPath: $downloadPath,
339
+ originalFilename: (if $originalFilename == "" then null else $originalFilename end)
340
+ }
341
+ }'
342
+ )"
343
+
344
+ work_product="$(
345
+ request_json \
346
+ POST \
347
+ "$api_base/issues/$issue_id/work-products" \
348
+ "$work_product_payload"
349
+ )"
350
+ fi
351
+
352
+ if [[ "$output_format" == "json" ]]; then
353
+ jq -n --argjson attachment "$attachment" --argjson workProduct "$work_product" \
354
+ '{attachment: $attachment, workProduct: $workProduct}'
355
+ exit 0
356
+ fi
357
+
358
+ content_path="$(jq -r '.contentPath // empty' <<<"$attachment")"
359
+ download_path="$(jq -r '.downloadPath // (if .contentPath then (.contentPath + "?download=1") else "" end)' <<<"$attachment")"
360
+ attachment_id="$(jq -r '.id // empty' <<<"$attachment")"
361
+ work_product_id="$(jq -r '.id // empty' <<<"$work_product")"
362
+
363
+ printf 'Uploaded artifact\n\n'
364
+ printf -- '- Attachment: [%s](%s)\n' "$title" "$content_path"
365
+ printf -- '- Download: [%s](%s)\n' "$title" "$download_path"
366
+ printf -- '- Attachment ID: `%s`\n' "$attachment_id"
367
+ if [[ -n "$work_product_id" ]]; then
368
+ printf -- '- Work product ID: `%s`\n' "$work_product_id"
369
+ fi
370
+ printf '\nFinal comment snippet:\n\n'
371
+ printf -- '- Artifact: [%s](%s)\n' "$title" "$content_path"