td-ai-tools 1.0.2 → 1.0.3
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 +1 -1
- package/skills/basecamp/SKILL.md +651 -0
- package/skills/everhour-basecamp-estimates/SKILL.md +2 -1
- package/skills/everhour-basecamp-estimates/scripts/update_estimates.py +157 -21
- package/skills/everhour-basecamp-estimates/tests/test_update_estimates.py +194 -1
- package/skills/pull-request/SKILL.md +1 -1
- package/skills/pull-request-statamic/SKILL.md +216 -0
- package/skills/pull-request-statamic/agents/openai.yaml +4 -0
package/package.json
CHANGED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: basecamp
|
|
3
|
+
description: |
|
|
4
|
+
Interact with Basecamp via the Basecamp CLI. Full API coverage: projects, todos, cards,
|
|
5
|
+
messages, files, schedule, check-ins, timeline, recordings, templates, webhooks,
|
|
6
|
+
subscriptions, lineup, and campfire. Use for ANY Basecamp question or action.
|
|
7
|
+
invocable: true
|
|
8
|
+
argument-hint: '[action] [args...]'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# /basecamp - Basecamp Workflow Command
|
|
12
|
+
|
|
13
|
+
Full CLI coverage: 130 endpoints across todos, cards, messages, files, schedule, check-ins, timeline, recordings, templates, webhooks, subscriptions, lineup, and campfire.
|
|
14
|
+
|
|
15
|
+
## Agent Invariants
|
|
16
|
+
|
|
17
|
+
**MUST follow these rules:**
|
|
18
|
+
|
|
19
|
+
1. **Choose the right output mode** — `--json` when you need to parse data; `--md` when presenting results to a human (see Output Modes below)
|
|
20
|
+
2. **Parse URLs first** with `basecamp url parse "<url>"` to extract IDs
|
|
21
|
+
3. **Comments are flat** - reply to parent recording, not to comments
|
|
22
|
+
4. **Check context** via `.basecamp/config.json` before assuming project
|
|
23
|
+
5. **Content fields accept Markdown** — message body and comment content accept Markdown syntax; the CLI converts to HTML automatically. Use Markdown formatting (lists, bold, links, code blocks) for rich content. For todos, documents, and cards, content is sent as-is — use plain text or HTML directly.
|
|
24
|
+
6. **Project scope is mandatory for most commands** — via `--in <project>` or `.basecamp/config.json`. Cross-project exceptions: `basecamp reports assigned` for assigned work, `basecamp reports overdue` for overdue todos, `basecamp recordings <type>` for browsing by type.
|
|
25
|
+
|
|
26
|
+
### Output Modes
|
|
27
|
+
|
|
28
|
+
**Choosing a mode:**
|
|
29
|
+
|
|
30
|
+
| Goal | Flag | Format |
|
|
31
|
+
| ---------------------- | ------------- | --------------------------------------------------------------------------------------------- |
|
|
32
|
+
| Parse data, pipe to jq | `--json` | JSON envelope: `{ok, data, summary, breadcrumbs, meta}` |
|
|
33
|
+
| Show results to a user | `--md` / `-m` | GFM tables, task lists, structured Markdown |
|
|
34
|
+
| Automation / scripting | `--agent` | Success: raw JSON data (no envelope); errors: `{ok:false,...}` object; no interactive prompts |
|
|
35
|
+
|
|
36
|
+
Always pass `--json` or `--md` explicitly — auto-detection depends on config and may not produce the format you expect. Use `--md` when composing reports, summarizing data, or displaying results inline. `--agent` is for headless integration scripts.
|
|
37
|
+
|
|
38
|
+
**Other modes:** `--quiet` (success: raw JSON, no envelope; errors: `{ok:false,...}`), `--ids-only`, `--count`, `--stats` (session statistics), `--styled` (force ANSI), `-v` / `-vv` (verbose/trace).
|
|
39
|
+
|
|
40
|
+
### CLI Introspection
|
|
41
|
+
|
|
42
|
+
Navigate unfamiliar commands with `--agent --help` — returns structured JSON describing any command:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
basecamp todos --agent --help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"command": "todos",
|
|
51
|
+
"path": "basecamp todos",
|
|
52
|
+
"short": "...",
|
|
53
|
+
"long": "...",
|
|
54
|
+
"usage": "...",
|
|
55
|
+
"notes": ["..."],
|
|
56
|
+
"subcommands": [{ "name": "sweep", "short": "...", "path": "basecamp todos sweep" }],
|
|
57
|
+
"flags": [{ "name": "assignee", "type": "string", "default": "", "usage": "..." }],
|
|
58
|
+
"inherited_flags": [
|
|
59
|
+
{ "name": "json", "shorthand": "j", "type": "bool", "default": "false", "usage": "..." }
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Walk the tree: start at `basecamp --agent --help` for top-level commands, then drill into any subcommand. Commands include `notes` with domain-specific agent hints (e.g., "Cards do NOT support --assignee filtering").
|
|
65
|
+
|
|
66
|
+
### Pagination
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
basecamp <cmd> --limit 50 # Cap results (default varies by resource)
|
|
70
|
+
basecamp <cmd> --all # Fetch all (may be slow for large datasets)
|
|
71
|
+
basecamp <cmd> --page 1 # First page only, no auto-pagination
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`--all` and `--limit` are mutually exclusive. `--page` cannot combine with either.
|
|
75
|
+
|
|
76
|
+
### Smart Defaults
|
|
77
|
+
|
|
78
|
+
- `--assignee me` resolves to current user
|
|
79
|
+
- `--due tomorrow` / `--due +3` / `--due "next week"` - natural date parsing
|
|
80
|
+
- Project from `.basecamp/config.json` if `--in` not specified
|
|
81
|
+
|
|
82
|
+
## Quick Reference
|
|
83
|
+
|
|
84
|
+
> **Note:** Most queries require project scope (via `--in <project>` or `.basecamp/config.json`). Cross-project exceptions: `basecamp reports assigned`, `basecamp reports overdue`, `basecamp recordings <type>`.
|
|
85
|
+
|
|
86
|
+
| Task | Command |
|
|
87
|
+
| ----------------------------- | ------------------------------------------------------------------------------- |
|
|
88
|
+
| List projects | `basecamp projects list --json` |
|
|
89
|
+
| My todos (in project) | `basecamp todos list --assignee me --in <project> --json` |
|
|
90
|
+
| My todos (cross-project) | `basecamp reports assigned --json` (defaults to "me") |
|
|
91
|
+
| All todos (cross-project) | `basecamp recordings todos --json` (no assignee data — cannot filter by person) |
|
|
92
|
+
| Overdue todos (in project) | `basecamp todos list --overdue --in <project> --json` |
|
|
93
|
+
| Overdue todos (cross-project) | `basecamp reports overdue --json` |
|
|
94
|
+
| Assign todo | `basecamp assign <id> --to <person> --in <project> --json` |
|
|
95
|
+
| Create todo | `basecamp todo "Task" --in <project> --list <list> --json` |
|
|
96
|
+
| Create todolist | `basecamp todolists create "Name" --in <project> --json` |
|
|
97
|
+
| Complete todo | `basecamp done <id> --json` |
|
|
98
|
+
| List cards | `basecamp cards list --in <project> --json` |
|
|
99
|
+
| Create card | `basecamp card "Title" --in <project> --json` |
|
|
100
|
+
| Move card | `basecamp cards move <id> --to <column> --in <project> --json` |
|
|
101
|
+
| Post message | `basecamp message "Title" "Body" --in <project> --json` |
|
|
102
|
+
| Post silently | `basecamp message "Title" "Body" --no-subscribe --in <project> --json` |
|
|
103
|
+
| Post to campfire | `basecamp campfire post "Message" --in <project> --json` |
|
|
104
|
+
| Add comment | `basecamp comment <recording_id> "Text" --in <project> --json` |
|
|
105
|
+
| Search | `basecamp search "query" --json` |
|
|
106
|
+
| Parse URL | `basecamp url parse "<url>" --json` |
|
|
107
|
+
| Download file | `basecamp files download <id> --in <project>` |
|
|
108
|
+
| Watch timeline | `basecamp timeline --watch` |
|
|
109
|
+
|
|
110
|
+
## URL Parsing
|
|
111
|
+
|
|
112
|
+
**Always parse URLs before acting on them:**
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
basecamp url parse "https://3.basecamp.com/2914079/buckets/41746046/messages/9478142982#__recording_9488783598" --json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Returns: `account_id`, `project_id`, `type`, `recording_id`, `comment_id` (from fragment).
|
|
119
|
+
|
|
120
|
+
**URL patterns:**
|
|
121
|
+
|
|
122
|
+
- `/buckets/27/messages/123` - Message 123 in project 27
|
|
123
|
+
- `/buckets/27/messages/123#__recording_456` - Comment 456 on message 123
|
|
124
|
+
- `/buckets/27/card_tables/cards/789` - Card 789
|
|
125
|
+
- `/buckets/27/card_tables/columns/456` - Column 456 (for creating cards)
|
|
126
|
+
- `/buckets/27/todos/101` - Todo 101
|
|
127
|
+
- `/buckets/27/uploads/202` - Upload/file 202
|
|
128
|
+
- `/buckets/27/documents/303` - Document 303
|
|
129
|
+
- `/buckets/27/schedule_entries/404` - Schedule entry 404
|
|
130
|
+
|
|
131
|
+
**Replying to comments:**
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Comments are flat - reply to the parent recording_id, not the comment_id
|
|
135
|
+
basecamp url parse "https://...messages/123#__recording_456" --json
|
|
136
|
+
# Returns recording_id: 123 (parent), comment_id: 456 (fragment) - comment on 123, not 456
|
|
137
|
+
basecamp comment 123 "Reply" --in <project>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Decision Trees
|
|
141
|
+
|
|
142
|
+
### Finding Content
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
Need to find something?
|
|
146
|
+
├── Know the type + project? → basecamp <type> list --in <project> --json
|
|
147
|
+
│ (some groups have default list behavior; use --agent --help if unsure)
|
|
148
|
+
├── My assigned work? → basecamp reports assigned --json (defaults to "me")
|
|
149
|
+
├── Overdue across projects? → basecamp reports overdue --json
|
|
150
|
+
├── Browse by type cross-project? → basecamp recordings <type> --json
|
|
151
|
+
│ (types: todos, messages, documents, comments, cards, uploads)
|
|
152
|
+
│ Note: Defaults to active status; use --status archived for archived items
|
|
153
|
+
│ ⚠ No assignee data — cannot filter by person; use reports assigned instead
|
|
154
|
+
├── Full-text search? → basecamp search "query" --json
|
|
155
|
+
└── Have a URL? → basecamp url parse "<url>" --json
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Modifying Content
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
Want to change something?
|
|
162
|
+
├── Have URL? → basecamp url parse "<url>" → use extracted IDs
|
|
163
|
+
├── Have ID? → basecamp <resource> update <id> --field value
|
|
164
|
+
├── Change status? → basecamp recordings trash|archive|restore <id>
|
|
165
|
+
└── Complete todo? → basecamp done <id>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Common Workflows
|
|
169
|
+
|
|
170
|
+
### Link Code to Basecamp Todo
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Get commit info and comment on todo (use printf %q for safe quoting)
|
|
174
|
+
COMMIT=$(git rev-parse --short HEAD)
|
|
175
|
+
MSG=$(git log -1 --format=%s)
|
|
176
|
+
basecamp comment <todo_id> "Commit $COMMIT: $(printf '%s' "$MSG")" --in <project>
|
|
177
|
+
|
|
178
|
+
# Complete when done
|
|
179
|
+
basecamp done <todo_id>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Track PR in Basecamp
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# Create todo for PR work
|
|
186
|
+
basecamp todo "Review PR #42" --in <project> --assignee me --due tomorrow
|
|
187
|
+
|
|
188
|
+
# When merged
|
|
189
|
+
basecamp done <todo_id>
|
|
190
|
+
basecamp campfire post "Merged PR #42" --in <project>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Bulk Process Overdue Todos
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Preview overdue todos
|
|
197
|
+
basecamp todos sweep --overdue --dry-run --in <project>
|
|
198
|
+
|
|
199
|
+
# Complete all with comment
|
|
200
|
+
basecamp todos sweep --overdue --complete --comment "Cleaning up" --in <project>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Move Card Through Workflow
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# List columns to get IDs
|
|
207
|
+
basecamp cards columns --in <project> --json
|
|
208
|
+
|
|
209
|
+
# Move card to column
|
|
210
|
+
basecamp cards move <card_id> --to <column_id> --in <project>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Download File from Basecamp
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
basecamp files download <upload_id> --in <project> --out ./downloads
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Resource Reference
|
|
220
|
+
|
|
221
|
+
### Projects
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
basecamp projects list --json # List all
|
|
225
|
+
basecamp projects show <id> --json # Show details
|
|
226
|
+
basecamp projects create "Name" --json # Create
|
|
227
|
+
basecamp projects update <id> --name "New" # Update
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Todos
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
basecamp todos list --in <project> --json # List in project
|
|
234
|
+
basecamp todos list --assignee me --in <project> # My todos
|
|
235
|
+
basecamp todos list --overdue --in <project> # Overdue only
|
|
236
|
+
basecamp todos list --status completed --in <project> # Completed
|
|
237
|
+
basecamp todos list --list <todolist_id> --in <project> # In specific list
|
|
238
|
+
basecamp todo "Task" --in <project> --list <list> --assignee me --due tomorrow
|
|
239
|
+
basecamp done <id> [id...] # Complete (multiple OK)
|
|
240
|
+
basecamp reopen <id> # Uncomplete
|
|
241
|
+
basecamp assign <id> --to <person> --in <project> # Assign (person: ID, email, or "me")
|
|
242
|
+
basecamp unassign <id> --from <person> --in <project> # Remove assignee
|
|
243
|
+
basecamp todos position <id> --to 1 # Move to top
|
|
244
|
+
basecamp todos sweep --overdue --complete --comment "Done" --in <project>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Flags:** `--assignee` (todos only - not available on cards/messages), `--status` (completed/pending), `--overdue`, `--list`, `--due`, `--limit`, `--all`
|
|
248
|
+
|
|
249
|
+
### Todolists
|
|
250
|
+
|
|
251
|
+
Todolists are containers for todos. Create a todolist before adding todos.
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
basecamp todolists list --in <project> --json # List todolists
|
|
255
|
+
basecamp todolists show <id> --in <project> # Show details
|
|
256
|
+
basecamp todolists create "Name" --in <project> --json # Create
|
|
257
|
+
basecamp todolists create "Name" --description "Desc" --in <project>
|
|
258
|
+
basecamp todolists update <id> --name "New" --in <project> # Update
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Cards (Kanban)
|
|
262
|
+
|
|
263
|
+
**Note:** Cards do NOT support `--assignee` filtering like todos. Fetch all cards and filter client-side if needed. If a project has multiple card tables, you must specify `--card-table <id>`. When you get an "Ambiguous card table" error, the hint shows available table IDs and names.
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
basecamp cards list --in <project> --json # All cards
|
|
267
|
+
basecamp cards list --card-table <id> --in <project> # Specific table (required if multiple)
|
|
268
|
+
basecamp cards list --column <id> --in <project> # Cards in column
|
|
269
|
+
basecamp cards columns --in <project> --json # List columns (needs --card-table if multiple)
|
|
270
|
+
basecamp cards show <id> --in <project> # Card details
|
|
271
|
+
basecamp card "Title" "<p>Body</p>" --in <project> --column <id>
|
|
272
|
+
basecamp cards update <id> --title "New" --due tomorrow --assignee me
|
|
273
|
+
basecamp cards move <id> --to <column_id> # Move to column (numeric ID)
|
|
274
|
+
basecamp cards move <id> --to "Done" --card-table <table_id> # Move by name (needs table)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Identifying completed cards:** Cards in Done columns have `parent.type: "Kanban::DoneColumn"` and `completed: true`. Use this to identify completed cards that haven't been archived.
|
|
278
|
+
|
|
279
|
+
**Limitation:** Basecamp does not track when cards are moved between columns. The `updated_at` field updates on any modification and cannot reliably indicate when a card was completed.
|
|
280
|
+
|
|
281
|
+
**Card Steps (checklists):**
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
basecamp cards steps <card_id> --in <project> # List steps
|
|
285
|
+
basecamp cards step create "Step" --card <id> --in <project>
|
|
286
|
+
basecamp cards step complete <step_id> --in <project>
|
|
287
|
+
basecamp cards step uncomplete <step_id>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Column management:**
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
basecamp cards column show <id> --in <project>
|
|
294
|
+
basecamp cards column create "Name" --in <project>
|
|
295
|
+
basecamp cards column update <id> --title "New"
|
|
296
|
+
basecamp cards column move <id> --position 2
|
|
297
|
+
basecamp cards column color <id> --color blue
|
|
298
|
+
basecamp cards column on-hold <id> # Enable on-hold section
|
|
299
|
+
basecamp cards column watch <id> # Subscribe to column
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Messages
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
basecamp messages list --in <project> --json # List messages
|
|
306
|
+
basecamp messages show <id> --in <project> # Show message
|
|
307
|
+
basecamp message "Title" "Body" --in <project>
|
|
308
|
+
basecamp messages update <id> --title "New" --body "Updated"
|
|
309
|
+
basecamp messages pin <id> --in <project> # Pin to top
|
|
310
|
+
basecamp messages unpin <id> # Unpin
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Flags:** `--draft` (create as draft), `--no-subscribe` (silent, no notifications), `--subscribe "people"` (comma-separated names, emails, IDs, or "me"; mutually exclusive with `--no-subscribe`), `--message-board <id>` (if multiple boards)
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
basecamp message "Bot update" "Done" --no-subscribe --in <project>
|
|
317
|
+
basecamp message "FYI" "Note" --subscribe "Alice,bob@x.com" --in <project>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Comments
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
basecamp comments list <recording_id> --in <project> --json
|
|
324
|
+
basecamp comment <recording_id> "Text" --in <project>
|
|
325
|
+
basecamp comments update <id> "Updated" --in <project>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Files & Documents
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
basecamp files list --in <project> --json # List all (folders, files, docs)
|
|
332
|
+
basecamp files list --vault <folder_id> --in <project> # List folder contents
|
|
333
|
+
basecamp files show <id> --in <project> # Show item (auto-detects type)
|
|
334
|
+
basecamp files download <id> --in <project> # Download file
|
|
335
|
+
basecamp files download <id> --out ./dir # Download to specific dir
|
|
336
|
+
basecamp files folder create "Folder" --in <project>
|
|
337
|
+
basecamp files doc create "Doc" "Body" --in <project>
|
|
338
|
+
basecamp files doc create "Draft" --draft --in <project>
|
|
339
|
+
basecamp files doc create "Notes" "..." --no-subscribe --in <project>
|
|
340
|
+
basecamp files update <id> --title "New" --content "Updated"
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Subcommands:** `folders`, `uploads`, `documents` (each with pagination flags)
|
|
344
|
+
|
|
345
|
+
### Schedule
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
basecamp schedule --in <project> --json # Schedule info
|
|
349
|
+
basecamp schedule entries --in <project> --json # List entries
|
|
350
|
+
basecamp schedule show <id> --in <project> # Entry details
|
|
351
|
+
basecamp schedule show <id> --date 20240315 # Specific occurrence (recurring)
|
|
352
|
+
basecamp schedule create "Event" --starts-at "2024-03-15T09:00:00Z" --ends-at "2024-03-15T10:00:00Z" --in <project>
|
|
353
|
+
basecamp schedule create "Meeting" --all-day --notify --participants 1,2,3 --in <project>
|
|
354
|
+
basecamp schedule create "Sync" --starts-at "..." --ends-at "..." --no-subscribe --in <project>
|
|
355
|
+
basecamp schedule update <id> --summary "New title" --starts-at "..."
|
|
356
|
+
basecamp schedule settings --include-due --in <project> # Include todos/cards due dates
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Flags:** `--all-day`, `--notify`, `--participants <ids>`, `--no-subscribe`, `--subscribe "people"` (mutually exclusive), `--status` (active/archived/trashed)
|
|
360
|
+
|
|
361
|
+
### Check-ins
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
basecamp checkins --in <project> --json # Questionnaire info
|
|
365
|
+
basecamp checkins questions --in <project> # List questions
|
|
366
|
+
basecamp checkins question <id> --in <project> # Question details
|
|
367
|
+
basecamp checkins answers <question_id> --in <project> # List answers
|
|
368
|
+
basecamp checkins answer <id> --in <project> # Answer details
|
|
369
|
+
basecamp checkins question create "What did you work on?" --in <project>
|
|
370
|
+
basecamp checkins question update <id> "New question" --frequency every_week
|
|
371
|
+
basecamp checkins answer create <question-id> "My answer" --in <project>
|
|
372
|
+
basecamp checkins answer update <id> "Updated" --in <project>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
**Schedule options:** `--frequency` (every_day, every_week, every_other_week, every_month, on_certain_days), `--days 1,2,3,4,5` (0=Sun), `--time "5:00pm"`
|
|
376
|
+
|
|
377
|
+
### Timeline
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
basecamp timeline --json # Account-wide activity
|
|
381
|
+
basecamp timeline --in <project> --json # Project activity
|
|
382
|
+
basecamp timeline me --json # Your activity
|
|
383
|
+
basecamp timeline --person <id> --json # Person's activity
|
|
384
|
+
basecamp timeline --watch # Live monitoring (TUI)
|
|
385
|
+
basecamp timeline --watch --interval 60 # Poll every 60 seconds
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Note:** `basecamp timeline` (account-wide) works reliably. The `--limit` flag is not supported on timeline commands.
|
|
389
|
+
|
|
390
|
+
### Recordings (Cross-project)
|
|
391
|
+
|
|
392
|
+
Use `basecamp recordings <type>` for cross-project type browsing. **For assigned todos, prefer `basecamp reports assigned`** — recordings do not include assignee data and cannot be filtered by person.
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
basecamp recordings todos --json # All todos across projects
|
|
396
|
+
basecamp recordings todos --all --json # All todos (paginate through all)
|
|
397
|
+
basecamp recordings messages --in <project> # Messages in project
|
|
398
|
+
basecamp recordings documents --status archived # Archived docs
|
|
399
|
+
basecamp recordings cards --sort created_at --direction asc
|
|
400
|
+
basecamp recordings cards --status archived --all --json # Include archived cards
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Types:** `todos`, `messages`, `documents`, `comments`, `cards`, `uploads`
|
|
404
|
+
|
|
405
|
+
**Status filtering:** By default, only `active` recordings are returned. Use `--status archived` or `--status trashed` to query other statuses. You may need separate queries to get complete data (e.g., active + archived).
|
|
406
|
+
|
|
407
|
+
**Status management:**
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
basecamp recordings trash <id> --in <project> # Move to trash
|
|
411
|
+
basecamp recordings archive <id> --in <project> # Archive
|
|
412
|
+
basecamp recordings restore <id> --in <project> # Restore to active
|
|
413
|
+
basecamp recordings visibility <id> --visible --in <project> # Show to clients
|
|
414
|
+
basecamp recordings visibility <id> --hidden # Hide from clients
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Templates
|
|
418
|
+
|
|
419
|
+
```bash
|
|
420
|
+
basecamp templates --json # List templates
|
|
421
|
+
basecamp templates show <id> --json # Template details
|
|
422
|
+
basecamp templates create "Template Name" # Create empty template
|
|
423
|
+
basecamp templates update <id> --name "New Name"
|
|
424
|
+
basecamp templates delete <id> # Trash template
|
|
425
|
+
basecamp templates construct <id> --name "New Project" # Create project (async)
|
|
426
|
+
basecamp templates construction <template_id> <construction_id> # Check status
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Construct returns construction_id - poll until status="completed" to get project.**
|
|
430
|
+
|
|
431
|
+
### Webhooks
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
basecamp webhooks list --in <project> --json # List webhooks
|
|
435
|
+
basecamp webhooks show <id> --in <project> # Webhook details
|
|
436
|
+
basecamp webhooks create "https://..." --in <project>
|
|
437
|
+
basecamp webhooks create "https://..." --types "Todo,Comment" --in <project>
|
|
438
|
+
basecamp webhooks update <id> --active --in <project>
|
|
439
|
+
basecamp webhooks update <id> --inactive # Disable
|
|
440
|
+
basecamp webhooks delete <id> --in <project>
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Event types:** Todo, Todolist, Message, Comment, Document, Upload, Vault, Schedule::Entry, Kanban::Card, Question, Question::Answer
|
|
444
|
+
|
|
445
|
+
### Subscriptions
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
basecamp subscriptions <recording_id> # Who's subscribed
|
|
449
|
+
basecamp subscriptions subscribe <id> # Subscribe yourself
|
|
450
|
+
basecamp subscriptions unsubscribe <id> # Unsubscribe
|
|
451
|
+
basecamp subscriptions add <id> --people 1,2,3 # Add people
|
|
452
|
+
basecamp subscriptions remove <id> --people 1,2,3 # Remove people
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Lineup (Account-wide Markers)
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
basecamp lineup create "Milestone" "2024-03-15" # Create marker
|
|
459
|
+
basecamp lineup create "Launch" tomorrow # Natural date parsing
|
|
460
|
+
basecamp lineup update <id> "New Name" "+7"
|
|
461
|
+
basecamp lineup delete <id>
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Note:** Lineup markers are account-wide, not project-scoped.
|
|
465
|
+
|
|
466
|
+
### Campfire
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
basecamp campfire --in <project> --json # List campfires
|
|
470
|
+
basecamp campfire messages --in <project> --json # List messages
|
|
471
|
+
basecamp campfire post "Hello!" --in <project>
|
|
472
|
+
basecamp campfire line <line_id> --in <project> # Show line
|
|
473
|
+
basecamp campfire delete <line_id> --in <project> # Delete line
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### People
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
basecamp people list --json # All people in account
|
|
480
|
+
basecamp people list --project <project> --json # People on project
|
|
481
|
+
basecamp me --json # Current user
|
|
482
|
+
basecamp people show <id> --json # Person details
|
|
483
|
+
basecamp people add <id> --project <project> # Add to project
|
|
484
|
+
basecamp people remove <id> --project <project> # Remove from project
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Search
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
basecamp search "query" --json # Full-text search
|
|
491
|
+
basecamp search "query" --sort updated_at --limit 20
|
|
492
|
+
basecamp search metadata --json # Available search scopes
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Generic Show
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
basecamp show <type> <id> --in <project> --json # Show any recording type
|
|
499
|
+
# Types: todo, todolist, message, comment, card, card-table, document (or omit <type> for generic lookup)
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## Configuration
|
|
503
|
+
|
|
504
|
+
The CLI uses two directory namespaces: `basecamp` for your Basecamp identity and project relationships, `basecamp` for tool-specific operational data.
|
|
505
|
+
|
|
506
|
+
```
|
|
507
|
+
~/.config/basecamp/ # Basecamp identity (DO NOT read credentials)
|
|
508
|
+
├── credentials.json # OAuth tokens — NEVER read or log
|
|
509
|
+
├── client.json # DCR client registration
|
|
510
|
+
└── config.json # Global preferences (account_id, base_url, format)
|
|
511
|
+
|
|
512
|
+
~/.cache/basecamp/ # Tool cache (ephemeral, auto-managed)
|
|
513
|
+
├── completion.json # Tab completion cache
|
|
514
|
+
└── resilience/ # Circuit breaker state
|
|
515
|
+
|
|
516
|
+
.basecamp/ # Per-repo config (committed to git)
|
|
517
|
+
└── config.json # Project defaults (project_id, account_id, todolist_id)
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Per-repo config:** `.basecamp/config.json`
|
|
521
|
+
|
|
522
|
+
```json
|
|
523
|
+
{
|
|
524
|
+
"project_id": "12345",
|
|
525
|
+
"todolist_id": "67890"
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Initialize:**
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
basecamp config init
|
|
533
|
+
basecamp config set project_id <id>
|
|
534
|
+
basecamp config set todolist_id <id>
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**Config Trust:**
|
|
538
|
+
|
|
539
|
+
Authority keys (`base_url`, `default_profile`, `profiles`) in local/repo configs are blocked until explicitly trusted. This prevents a cloned repo's config from redirecting OAuth tokens.
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
basecamp config trust # Trust nearest .basecamp/config.json
|
|
543
|
+
basecamp config trust /path/to/.basecamp/config.json # Trust specific config file
|
|
544
|
+
basecamp config trust --list # Show all trusted configs
|
|
545
|
+
basecamp config untrust # Revoke trust for nearest config
|
|
546
|
+
basecamp config untrust /path/to/.basecamp/config.json # Revoke trust for specific path
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Check context:**
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
cat .basecamp/config.json 2>/dev/null || echo "No project configured"
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Global config:** `~/.config/basecamp/config.json` (account_id, base_url, format preferences)
|
|
556
|
+
|
|
557
|
+
## Error Handling
|
|
558
|
+
|
|
559
|
+
**General diagnostics:**
|
|
560
|
+
|
|
561
|
+
```bash
|
|
562
|
+
basecamp doctor --json # Check CLI health, auth, connectivity
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Rate limiting (429):** The CLI handles backoff automatically. If you see 429 errors, reduce request frequency.
|
|
566
|
+
|
|
567
|
+
**Authentication errors:**
|
|
568
|
+
|
|
569
|
+
```bash
|
|
570
|
+
basecamp auth status # Check auth
|
|
571
|
+
basecamp auth login # Re-authenticate
|
|
572
|
+
basecamp auth login --scope full # Full access (BC3 OAuth only)
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Network errors / localhost URLs:**
|
|
576
|
+
|
|
577
|
+
```bash
|
|
578
|
+
# Check for dev config
|
|
579
|
+
cat ~/.config/basecamp/config.json
|
|
580
|
+
# Should only contain: {"account_id": "<id>"}
|
|
581
|
+
# Remove base_url/api_url if pointing to localhost
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
**Not found errors:**
|
|
585
|
+
|
|
586
|
+
```bash
|
|
587
|
+
basecamp auth status # Verify auth working
|
|
588
|
+
cat ~/.config/basecamp/accounts.json # Check available accounts
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Required arguments are positional (not flags):**
|
|
592
|
+
|
|
593
|
+
- `basecamp todo "Buy milk"` (not `--content`)
|
|
594
|
+
- `basecamp card "New feature"` (not `--title`)
|
|
595
|
+
- `basecamp message "Subject" "Body"` (not `--subject`)
|
|
596
|
+
- `basecamp campfire post "Hello"` (not `--content`)
|
|
597
|
+
- `basecamp comment <id> "Text"` (not a flag)
|
|
598
|
+
- `basecamp webhooks create "https://..." --in <project>` (not `--url`)
|
|
599
|
+
- `basecamp checkins answer create <question-id> "content"` (not `--question`)
|
|
600
|
+
|
|
601
|
+
**Missing argument errors (code: "usage"):**
|
|
602
|
+
When a required positional argument is missing, the CLI returns a structured error naming
|
|
603
|
+
the specific argument. Use this for elicitation:
|
|
604
|
+
|
|
605
|
+
```bash
|
|
606
|
+
$ basecamp todo --json
|
|
607
|
+
{"ok": false, "error": "<content> required", "code": "usage",
|
|
608
|
+
"hint": "Usage: basecamp todo <content>"}
|
|
609
|
+
|
|
610
|
+
$ basecamp comments create 123 --json
|
|
611
|
+
{"ok": false, "error": "<content> required", "code": "usage", ...}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
The `error` field names the missing `<arg>` — use it to prompt the user for the specific value.
|
|
615
|
+
|
|
616
|
+
**URL malformed (curl exit 3):** Special characters in content. Use plain text or properly escaped HTML.
|
|
617
|
+
|
|
618
|
+
## jq Patterns
|
|
619
|
+
|
|
620
|
+
Common data extraction patterns for the output envelope:
|
|
621
|
+
|
|
622
|
+
```bash
|
|
623
|
+
# Extract fields from data array
|
|
624
|
+
basecamp todos list --in <project> --json | jq '.data[] | select(.completed == false) | .title'
|
|
625
|
+
basecamp todos list --in <project> --json | jq '.data | length'
|
|
626
|
+
basecamp todos list --in <project> --json | jq '.data[] | {id, title, status}'
|
|
627
|
+
|
|
628
|
+
# Access envelope metadata
|
|
629
|
+
basecamp todos list --in <project> --json | jq '.breadcrumbs[0].cmd'
|
|
630
|
+
basecamp todos list --in <project> --json | jq '.meta.stats.requests'
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
## Exit Codes
|
|
634
|
+
|
|
635
|
+
| Exit | Meaning | Fix |
|
|
636
|
+
| ---- | ------------- | ------------------------------------------------------------------- |
|
|
637
|
+
| 0 | OK | — |
|
|
638
|
+
| 1 | Usage error | Check `basecamp <cmd> --help` |
|
|
639
|
+
| 2 | Not found | Verify ID/URL exists |
|
|
640
|
+
| 3 | Auth error | `basecamp auth login` |
|
|
641
|
+
| 4 | Forbidden | Check account/project permissions |
|
|
642
|
+
| 5 | Rate limit | Wait and retry (resilience layer handles Retry-After automatically) |
|
|
643
|
+
| 6 | Network error | Check connectivity, `basecamp doctor` |
|
|
644
|
+
| 7 | API error | Retry; if persistent, check `basecamp doctor` |
|
|
645
|
+
| 8 | Ambiguous | Be more specific (use ID instead of name) |
|
|
646
|
+
|
|
647
|
+
## Learn More
|
|
648
|
+
|
|
649
|
+
- API concepts: https://github.com/basecamp/bc3-api#key-concepts
|
|
650
|
+
- CLI repo: https://github.com/basecamp/basecamp-cli
|
|
651
|
+
- API coverage: See API-COVERAGE.md in the CLI repo
|
|
@@ -56,6 +56,7 @@ python3 .agents/skills/everhour-basecamp-estimates/scripts/update_estimates.py \
|
|
|
56
56
|
- A scalar estimate is applied to each active todo.
|
|
57
57
|
- A JSON estimate array is applied in active-todo order and must match the active todo count exactly.
|
|
58
58
|
- It matches Basecamp titles against Everhour task names inside the configured project.
|
|
59
|
+
- If multiple Everhour tasks have the same normalized title, it selects the task whose Everhour task id or URL maps to the Basecamp todo id.
|
|
59
60
|
- Matching is exact after normalization:
|
|
60
61
|
- trim whitespace
|
|
61
62
|
- collapse repeated spaces
|
|
@@ -67,7 +68,7 @@ python3 .agents/skills/everhour-basecamp-estimates/scripts/update_estimates.py \
|
|
|
67
68
|
## Guardrails
|
|
68
69
|
|
|
69
70
|
- If any Basecamp title has no Everhour match, stop and report it.
|
|
70
|
-
- If any Basecamp title matches more than one Everhour task, stop and report the ambiguity.
|
|
71
|
+
- If any Basecamp title matches more than one Everhour task and the Basecamp todo id cannot resolve one task, stop and report the ambiguity.
|
|
71
72
|
- If an estimate array length does not match the active todo count, stop before any updates.
|
|
72
73
|
- If the provided todo URL is already completed, stop without updating anything.
|
|
73
74
|
- Do not update Basecamp titles before Everhour updates complete.
|
|
@@ -27,6 +27,7 @@ WHITESPACE_RE = re.compile(r"\s+")
|
|
|
27
27
|
class TodoItem:
|
|
28
28
|
id: str
|
|
29
29
|
title: str
|
|
30
|
+
api_url: str | None = None
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
@dataclass(frozen=True)
|
|
@@ -200,6 +201,13 @@ def extract_title(record: dict[str, Any]) -> str:
|
|
|
200
201
|
raise ScriptError(f"Could not determine a title from Basecamp payload: {record}")
|
|
201
202
|
|
|
202
203
|
|
|
204
|
+
def extract_api_url(record: dict[str, Any]) -> str | None:
|
|
205
|
+
value = record.get("url")
|
|
206
|
+
if isinstance(value, str) and value.strip():
|
|
207
|
+
return value.strip()
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
203
211
|
def extract_todo_objects(payload: Any) -> list[dict[str, Any]]:
|
|
204
212
|
if isinstance(payload, list):
|
|
205
213
|
candidates = [item for item in payload if is_todo_like(item)]
|
|
@@ -258,17 +266,46 @@ def is_todo_completed(record: dict[str, Any]) -> bool:
|
|
|
258
266
|
def load_basecamp_todos(basecamp_bin: str, basecamp_url: str) -> TodoSelection:
|
|
259
267
|
parsed = run_basecamp_json(basecamp_bin, "url", "parse", basecamp_url)
|
|
260
268
|
resource_type = str(parsed.get("type", "")).strip().lower()
|
|
269
|
+
project_id = str(parsed.get("project_id", "")).strip()
|
|
270
|
+
recording_id = str(parsed.get("recording_id", "")).strip()
|
|
261
271
|
|
|
262
272
|
if resource_type in {"todo", "todos"}:
|
|
263
273
|
payload = run_basecamp_json(basecamp_bin, "todos", "show", basecamp_url)
|
|
264
|
-
todo = TodoItem(
|
|
274
|
+
todo = TodoItem(
|
|
275
|
+
id=str(payload["id"]),
|
|
276
|
+
title=extract_title(payload),
|
|
277
|
+
api_url=extract_api_url(payload),
|
|
278
|
+
)
|
|
265
279
|
if is_todo_completed(payload):
|
|
266
280
|
return TodoSelection(active=[], skipped_completed=[todo])
|
|
267
281
|
return TodoSelection(active=[todo], skipped_completed=[])
|
|
268
282
|
|
|
269
283
|
if resource_type in {"todolist", "todolists"}:
|
|
270
|
-
payload =
|
|
271
|
-
todo_objects =
|
|
284
|
+
payload: Any = None
|
|
285
|
+
todo_objects: list[dict[str, Any]] = []
|
|
286
|
+
try:
|
|
287
|
+
payload = run_basecamp_json(basecamp_bin, "todolists", "show", basecamp_url)
|
|
288
|
+
except ScriptError:
|
|
289
|
+
payload = None
|
|
290
|
+
|
|
291
|
+
if payload is not None:
|
|
292
|
+
todo_objects = extract_todo_objects(payload)
|
|
293
|
+
|
|
294
|
+
# Some Basecamp CLI versions return an empty payload for todolists show even
|
|
295
|
+
# though the list exists. Fall back to listing todos scoped to the parsed
|
|
296
|
+
# project and todolist IDs.
|
|
297
|
+
if not todo_objects and project_id and recording_id:
|
|
298
|
+
fallback_payload = run_basecamp_json(
|
|
299
|
+
basecamp_bin,
|
|
300
|
+
"todos",
|
|
301
|
+
"list",
|
|
302
|
+
"--in",
|
|
303
|
+
project_id,
|
|
304
|
+
"--todolist",
|
|
305
|
+
recording_id,
|
|
306
|
+
)
|
|
307
|
+
todo_objects = extract_todo_objects(fallback_payload)
|
|
308
|
+
|
|
272
309
|
if not todo_objects:
|
|
273
310
|
raise ScriptError(
|
|
274
311
|
"The Basecamp todolist payload did not include any todo items. "
|
|
@@ -277,7 +314,11 @@ def load_basecamp_todos(basecamp_bin: str, basecamp_url: str) -> TodoSelection:
|
|
|
277
314
|
active: list[TodoItem] = []
|
|
278
315
|
skipped_completed: list[TodoItem] = []
|
|
279
316
|
for item in todo_objects:
|
|
280
|
-
todo = TodoItem(
|
|
317
|
+
todo = TodoItem(
|
|
318
|
+
id=str(item["id"]),
|
|
319
|
+
title=extract_title(item),
|
|
320
|
+
api_url=extract_api_url(item),
|
|
321
|
+
)
|
|
281
322
|
if is_todo_completed(item):
|
|
282
323
|
skipped_completed.append(todo)
|
|
283
324
|
else:
|
|
@@ -370,14 +411,12 @@ class EverhourClient:
|
|
|
370
411
|
raise ScriptError("Everhour task search response was not a list.")
|
|
371
412
|
return [task for task in tasks if isinstance(task, dict)]
|
|
372
413
|
|
|
373
|
-
def match_task(self, title: str) -> dict[str, Any]:
|
|
414
|
+
def match_task(self, title: str, source_id: str) -> dict[str, Any]:
|
|
374
415
|
normalized = normalize_title(title)
|
|
375
416
|
open_matches = self.list_open_tasks().get(normalized, [])
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
matches = ", ".join(str(task.get("id")) for task in open_matches)
|
|
380
|
-
raise ScriptError(f"Ambiguous Everhour matches for '{title}': {matches}")
|
|
417
|
+
open_match = select_single_task_match(title, open_matches, source_id)
|
|
418
|
+
if open_match is not None:
|
|
419
|
+
return open_match
|
|
381
420
|
|
|
382
421
|
search_matches = [
|
|
383
422
|
task
|
|
@@ -385,13 +424,12 @@ class EverhourClient:
|
|
|
385
424
|
if normalize_title(str(task.get("name", ""))) == normalized
|
|
386
425
|
]
|
|
387
426
|
if not search_matches:
|
|
388
|
-
raise ScriptError(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return search_matches[0]
|
|
427
|
+
raise ScriptError(format_missing_match_error(title, self.project_id))
|
|
428
|
+
search_match = select_single_task_match(title, search_matches, source_id)
|
|
429
|
+
if search_match is not None:
|
|
430
|
+
return search_match
|
|
431
|
+
|
|
432
|
+
raise ScriptError(format_ambiguous_match_error(title, search_matches, source_id))
|
|
395
433
|
|
|
396
434
|
def update_task_estimate(self, task_id: str, total_seconds: int) -> None:
|
|
397
435
|
self._request(
|
|
@@ -401,8 +439,106 @@ class EverhourClient:
|
|
|
401
439
|
)
|
|
402
440
|
|
|
403
441
|
|
|
404
|
-
def
|
|
405
|
-
|
|
442
|
+
def task_matches_source_id(task: dict[str, Any], source_id: str) -> bool:
|
|
443
|
+
normalized_source_id = str(source_id).strip()
|
|
444
|
+
if not normalized_source_id:
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
task_id = str(task.get("id", "")).strip()
|
|
448
|
+
if task_id == normalized_source_id or task_id.endswith(f":{normalized_source_id}"):
|
|
449
|
+
return True
|
|
450
|
+
|
|
451
|
+
url = str(task.get("url", "")).strip().rstrip("/")
|
|
452
|
+
return bool(url) and url.endswith(f"/{normalized_source_id}")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def select_single_task_match(
|
|
456
|
+
title: str,
|
|
457
|
+
tasks: list[dict[str, Any]],
|
|
458
|
+
source_id: str,
|
|
459
|
+
) -> dict[str, Any] | None:
|
|
460
|
+
if len(tasks) == 1:
|
|
461
|
+
return tasks[0]
|
|
462
|
+
if len(tasks) == 0:
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
source_matches = [task for task in tasks if task_matches_source_id(task, source_id)]
|
|
466
|
+
if len(source_matches) == 1:
|
|
467
|
+
return source_matches[0]
|
|
468
|
+
if len(source_matches) > 1:
|
|
469
|
+
raise ScriptError(format_ambiguous_match_error(title, source_matches, source_id))
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def format_ambiguous_match_error(
|
|
474
|
+
title: str,
|
|
475
|
+
matches: list[dict[str, Any]],
|
|
476
|
+
source_id: str,
|
|
477
|
+
) -> str:
|
|
478
|
+
match_ids = ", ".join(str(task.get("id")) for task in matches)
|
|
479
|
+
return (
|
|
480
|
+
f"Ambiguous Everhour matches for '{title}': {match_ids}. "
|
|
481
|
+
f"None could be uniquely matched to Basecamp todo {source_id}."
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def format_missing_match_error(title: str, project_id: str) -> str:
|
|
486
|
+
return f"No Everhour task match found for Basecamp title '{title}' in project {project_id}."
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def update_basecamp_title(basecamp_bin: str, todo: TodoItem, title: str) -> None:
|
|
490
|
+
if not todo.api_url:
|
|
491
|
+
raise ScriptError(f"Basecamp todo {todo.id} payload did not include an API URL.")
|
|
492
|
+
current = run_basecamp_json(basecamp_bin, "api", "get", todo.api_url)
|
|
493
|
+
if not isinstance(current, dict):
|
|
494
|
+
raise ScriptError(
|
|
495
|
+
f"Basecamp todo {todo.id} GET did not return a JSON object: {current!r}"
|
|
496
|
+
)
|
|
497
|
+
payload = build_basecamp_update_payload(current, title)
|
|
498
|
+
run_basecamp_json(
|
|
499
|
+
basecamp_bin,
|
|
500
|
+
"api",
|
|
501
|
+
"put",
|
|
502
|
+
todo.api_url,
|
|
503
|
+
"-d",
|
|
504
|
+
json.dumps(payload),
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def build_basecamp_update_payload(current: dict[str, Any], title: str) -> dict[str, Any]:
|
|
509
|
+
# Basecamp's PUT to a todo replaces all writable fields, so any field omitted
|
|
510
|
+
# here gets cleared. Carry over the assignees, completion subscribers, notes
|
|
511
|
+
# (description), and dates from the current record so we only change `content`.
|
|
512
|
+
payload: dict[str, Any] = {"content": title}
|
|
513
|
+
|
|
514
|
+
description = current.get("description")
|
|
515
|
+
if isinstance(description, str) and description:
|
|
516
|
+
payload["description"] = description
|
|
517
|
+
|
|
518
|
+
assignee_ids = _extract_person_ids(current.get("assignees"))
|
|
519
|
+
if assignee_ids:
|
|
520
|
+
payload["assignee_ids"] = assignee_ids
|
|
521
|
+
|
|
522
|
+
subscriber_ids = _extract_person_ids(current.get("completion_subscribers"))
|
|
523
|
+
if subscriber_ids:
|
|
524
|
+
payload["completion_subscriber_ids"] = subscriber_ids
|
|
525
|
+
|
|
526
|
+
for key in ("due_on", "starts_on"):
|
|
527
|
+
value = current.get(key)
|
|
528
|
+
if isinstance(value, str) and value:
|
|
529
|
+
payload[key] = value
|
|
530
|
+
|
|
531
|
+
return payload
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _extract_person_ids(people: Any) -> list[Any]:
|
|
535
|
+
if not isinstance(people, list):
|
|
536
|
+
return []
|
|
537
|
+
ids: list[Any] = []
|
|
538
|
+
for person in people:
|
|
539
|
+
if isinstance(person, dict) and "id" in person:
|
|
540
|
+
ids.append(person["id"])
|
|
541
|
+
return ids
|
|
406
542
|
|
|
407
543
|
|
|
408
544
|
def build_plan(
|
|
@@ -430,7 +566,7 @@ def build_plan(
|
|
|
430
566
|
)
|
|
431
567
|
seen_titles[normalized_title] = todo
|
|
432
568
|
|
|
433
|
-
task = everhour.match_task(todo.title)
|
|
569
|
+
task = everhour.match_task(todo.title, source_id=todo.id)
|
|
434
570
|
task_id = str(task.get("id", "")).strip()
|
|
435
571
|
task_name = str(task.get("name", "")).strip()
|
|
436
572
|
if not task_id or not task_name:
|
|
@@ -504,7 +640,7 @@ def main() -> int:
|
|
|
504
640
|
everhour.update_task_estimate(plan.everhour_task_id, plan.seconds)
|
|
505
641
|
|
|
506
642
|
for plan in plans:
|
|
507
|
-
update_basecamp_title(args.basecamp_bin, plan.todo
|
|
643
|
+
update_basecamp_title(args.basecamp_bin, plan.todo, plan.new_basecamp_title)
|
|
508
644
|
|
|
509
645
|
print("Completed Everhour estimate updates and Basecamp title updates.")
|
|
510
646
|
return 0
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import importlib.util
|
|
2
|
+
import json
|
|
2
3
|
import sys
|
|
3
4
|
import unittest
|
|
4
5
|
from decimal import Decimal
|
|
@@ -65,7 +66,7 @@ class BuildPlanTest(unittest.TestCase):
|
|
|
65
66
|
MODULE.build_plan(todos, estimates, everhour)
|
|
66
67
|
|
|
67
68
|
self.assertIn("duplicates todo 1", str(caught.exception))
|
|
68
|
-
everhour.match_task.assert_called_once_with("Ship estimate")
|
|
69
|
+
everhour.match_task.assert_called_once_with("Ship estimate", source_id="1")
|
|
69
70
|
|
|
70
71
|
def test_rejects_duplicate_everhour_task_matches(self) -> None:
|
|
71
72
|
todos = [
|
|
@@ -88,6 +89,198 @@ class BuildPlanTest(unittest.TestCase):
|
|
|
88
89
|
self.assertIn("todo 1 and todo 2", str(caught.exception))
|
|
89
90
|
self.assertEqual(everhour.match_task.call_count, 2)
|
|
90
91
|
|
|
92
|
+
def test_passes_basecamp_todo_id_to_everhour_matcher(self) -> None:
|
|
93
|
+
todos = [MODULE.TodoItem(id="1", title="Ship estimate")]
|
|
94
|
+
estimates = [(Decimal("1"), 3600)]
|
|
95
|
+
everhour = mock.Mock()
|
|
96
|
+
everhour.match_task.return_value = {
|
|
97
|
+
"id": "abc123",
|
|
98
|
+
"name": "Ship estimate",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
plans = MODULE.build_plan(todos, estimates, everhour)
|
|
102
|
+
|
|
103
|
+
everhour.match_task.assert_called_once_with("Ship estimate", source_id="1")
|
|
104
|
+
self.assertEqual(plans[0].everhour_task_id, "abc123")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class EverhourClientMatchTaskTest(unittest.TestCase):
|
|
108
|
+
def test_duplicate_open_matches_are_resolved_by_basecamp_todo_id(self) -> None:
|
|
109
|
+
client = MODULE.EverhourClient(api_key="key", project_id="project")
|
|
110
|
+
client.list_open_tasks = mock.Mock(
|
|
111
|
+
return_value={
|
|
112
|
+
"ship estimate": [
|
|
113
|
+
{"id": "b3:1", "name": "Ship estimate"},
|
|
114
|
+
{"id": "b3:2", "name": "Ship estimate"},
|
|
115
|
+
],
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
client.search_tasks = mock.Mock(return_value=[])
|
|
119
|
+
|
|
120
|
+
task = client.match_task("Ship estimate", source_id="2")
|
|
121
|
+
|
|
122
|
+
self.assertEqual(task["id"], "b3:2")
|
|
123
|
+
client.search_tasks.assert_not_called()
|
|
124
|
+
|
|
125
|
+
def test_duplicate_open_matches_still_fail_without_todo_id_match(self) -> None:
|
|
126
|
+
client = MODULE.EverhourClient(api_key="key", project_id="project")
|
|
127
|
+
client.list_open_tasks = mock.Mock(
|
|
128
|
+
return_value={
|
|
129
|
+
"ship estimate": [
|
|
130
|
+
{"id": "b3:1", "name": "Ship estimate"},
|
|
131
|
+
{"id": "b3:2", "name": "Ship estimate"},
|
|
132
|
+
],
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
client.search_tasks = mock.Mock(
|
|
136
|
+
return_value=[
|
|
137
|
+
{"id": "b3:1", "name": "Ship estimate"},
|
|
138
|
+
{"id": "b3:2", "name": "Ship estimate"},
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
with self.assertRaises(MODULE.ScriptError) as caught:
|
|
143
|
+
client.match_task("Ship estimate", source_id="3")
|
|
144
|
+
|
|
145
|
+
self.assertIn("Basecamp todo 3", str(caught.exception))
|
|
146
|
+
|
|
147
|
+
def test_duplicate_search_matches_are_resolved_by_basecamp_todo_id(self) -> None:
|
|
148
|
+
client = MODULE.EverhourClient(api_key="key", project_id="project")
|
|
149
|
+
client.list_open_tasks = mock.Mock(return_value={})
|
|
150
|
+
client.search_tasks = mock.Mock(
|
|
151
|
+
return_value=[
|
|
152
|
+
{"id": "b3:1", "name": "Ship estimate"},
|
|
153
|
+
{"id": "b3:2", "name": "Ship estimate"},
|
|
154
|
+
]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
task = client.match_task("Ship estimate", source_id="2")
|
|
158
|
+
|
|
159
|
+
self.assertEqual(task["id"], "b3:2")
|
|
160
|
+
client.search_tasks.assert_called_once_with("Ship estimate")
|
|
161
|
+
|
|
162
|
+
def test_basecamp_todo_id_can_match_task_url(self) -> None:
|
|
163
|
+
task = {
|
|
164
|
+
"id": "opaque",
|
|
165
|
+
"name": "Ship estimate",
|
|
166
|
+
"url": "https://3.basecamp.com/1/buckets/2/todos/123",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
self.assertTrue(MODULE.task_matches_source_id(task, "123"))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class UpdateBasecampTitleTest(unittest.TestCase):
|
|
173
|
+
def test_fetches_current_todo_then_puts_with_only_changed_content(self) -> None:
|
|
174
|
+
todo = MODULE.TodoItem(
|
|
175
|
+
id="123",
|
|
176
|
+
title="Ship estimate",
|
|
177
|
+
api_url="https://3.basecampapi.com/1/buckets/2/todos/123.json",
|
|
178
|
+
)
|
|
179
|
+
current_payload = {
|
|
180
|
+
"id": 123,
|
|
181
|
+
"content": "Ship estimate",
|
|
182
|
+
"description": "<div>Original notes with image</div>",
|
|
183
|
+
"assignees": [{"id": 7}, {"id": 8}],
|
|
184
|
+
"completion_subscribers": [{"id": 9}],
|
|
185
|
+
"due_on": "2026-05-01",
|
|
186
|
+
"starts_on": None,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
with mock.patch.object(MODULE, "run_basecamp_json") as run_basecamp_json:
|
|
190
|
+
run_basecamp_json.side_effect = [current_payload, {}]
|
|
191
|
+
MODULE.update_basecamp_title("basecamp", todo, "Ship estimate [2h]")
|
|
192
|
+
|
|
193
|
+
self.assertEqual(run_basecamp_json.call_count, 2)
|
|
194
|
+
get_call, put_call = run_basecamp_json.call_args_list
|
|
195
|
+
|
|
196
|
+
self.assertEqual(
|
|
197
|
+
get_call.args,
|
|
198
|
+
(
|
|
199
|
+
"basecamp",
|
|
200
|
+
"api",
|
|
201
|
+
"get",
|
|
202
|
+
"https://3.basecampapi.com/1/buckets/2/todos/123.json",
|
|
203
|
+
),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
self.assertEqual(put_call.args[:5], (
|
|
207
|
+
"basecamp",
|
|
208
|
+
"api",
|
|
209
|
+
"put",
|
|
210
|
+
"https://3.basecampapi.com/1/buckets/2/todos/123.json",
|
|
211
|
+
"-d",
|
|
212
|
+
))
|
|
213
|
+
body = json.loads(put_call.args[5])
|
|
214
|
+
self.assertEqual(body["content"], "Ship estimate [2h]")
|
|
215
|
+
self.assertEqual(body["description"], "<div>Original notes with image</div>")
|
|
216
|
+
self.assertEqual(body["assignee_ids"], [7, 8])
|
|
217
|
+
self.assertEqual(body["completion_subscriber_ids"], [9])
|
|
218
|
+
self.assertEqual(body["due_on"], "2026-05-01")
|
|
219
|
+
self.assertNotIn("starts_on", body)
|
|
220
|
+
|
|
221
|
+
def test_omits_fields_absent_from_current_todo(self) -> None:
|
|
222
|
+
todo = MODULE.TodoItem(
|
|
223
|
+
id="123",
|
|
224
|
+
title="Ship estimate",
|
|
225
|
+
api_url="https://3.basecampapi.com/1/buckets/2/todos/123.json",
|
|
226
|
+
)
|
|
227
|
+
current_payload = {
|
|
228
|
+
"id": 123,
|
|
229
|
+
"content": "Ship estimate",
|
|
230
|
+
"description": "",
|
|
231
|
+
"assignees": [],
|
|
232
|
+
"completion_subscribers": [],
|
|
233
|
+
"due_on": None,
|
|
234
|
+
"starts_on": None,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
with mock.patch.object(MODULE, "run_basecamp_json") as run_basecamp_json:
|
|
238
|
+
run_basecamp_json.side_effect = [current_payload, {}]
|
|
239
|
+
MODULE.update_basecamp_title("basecamp", todo, "Ship estimate [2h]")
|
|
240
|
+
|
|
241
|
+
put_call = run_basecamp_json.call_args_list[1]
|
|
242
|
+
body = json.loads(put_call.args[5])
|
|
243
|
+
self.assertEqual(body, {"content": "Ship estimate [2h]"})
|
|
244
|
+
|
|
245
|
+
def test_rejects_missing_basecamp_api_url(self) -> None:
|
|
246
|
+
todo = MODULE.TodoItem(id="123", title="Ship estimate")
|
|
247
|
+
|
|
248
|
+
with self.assertRaises(MODULE.ScriptError) as caught:
|
|
249
|
+
MODULE.update_basecamp_title("basecamp", todo, "Ship estimate [2h]")
|
|
250
|
+
|
|
251
|
+
self.assertIn("did not include an API URL", str(caught.exception))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class BuildBasecampUpdatePayloadTest(unittest.TestCase):
|
|
255
|
+
def test_extracts_assignee_and_subscriber_ids_and_preserves_dates(self) -> None:
|
|
256
|
+
current = {
|
|
257
|
+
"content": "Old",
|
|
258
|
+
"description": "<p>note</p>",
|
|
259
|
+
"assignees": [{"id": 1}, {"id": 2}],
|
|
260
|
+
"completion_subscribers": [{"id": 3}],
|
|
261
|
+
"due_on": "2026-05-01",
|
|
262
|
+
"starts_on": "2026-04-01",
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
payload = MODULE.build_basecamp_update_payload(current, "New title")
|
|
266
|
+
|
|
267
|
+
self.assertEqual(
|
|
268
|
+
payload,
|
|
269
|
+
{
|
|
270
|
+
"content": "New title",
|
|
271
|
+
"description": "<p>note</p>",
|
|
272
|
+
"assignee_ids": [1, 2],
|
|
273
|
+
"completion_subscriber_ids": [3],
|
|
274
|
+
"due_on": "2026-05-01",
|
|
275
|
+
"starts_on": "2026-04-01",
|
|
276
|
+
},
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def test_skips_empty_or_missing_fields(self) -> None:
|
|
280
|
+
payload = MODULE.build_basecamp_update_payload({"content": "Old"}, "New title")
|
|
281
|
+
|
|
282
|
+
self.assertEqual(payload, {"content": "New title"})
|
|
283
|
+
|
|
91
284
|
|
|
92
285
|
if __name__ == "__main__":
|
|
93
286
|
unittest.main()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: pull
|
|
2
|
+
name: pull-request
|
|
3
3
|
description: Generates well-structured GitHub pull request descriptions for Shopify theme development teams by analyzing git diffs and gathering context. Use this skill whenever a developer asks to create a PR, open a pull request, write a PR description, or submit code for review — even if they just say "make a PR" or "create a pull request". Also trigger when someone mentions needing a PR title, PR summary, or PR testing steps.
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pull-request-statamic
|
|
3
|
+
description: Generates GitHub pull request descriptions for Statamic and Laravel development by analyzing git diffs and gathering context.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# PR Description Generator
|
|
7
|
+
|
|
8
|
+
Helps Statamic and Laravel developers create well-structured pull request descriptions by analyzing code changes and collecting context. Follow these steps in order.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Step 1: Get Branch Info
|
|
13
|
+
|
|
14
|
+
Ask the developer for:
|
|
15
|
+
|
|
16
|
+
1. **Source branch** — the feature/fix branch with their changes
|
|
17
|
+
2. **Target branch** — the branch to merge into (e.g. `main`, `develop`, `release/x.x`)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Step 2: Analyze the Diff
|
|
22
|
+
|
|
23
|
+
Run this command to fetch and diff the branches:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
git fetch origin && git diff origin/{target_branch}...origin/{source_branch}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Analyze the diff for:
|
|
30
|
+
|
|
31
|
+
- Files changed, added, or deleted
|
|
32
|
+
- Nature of changes (feature, bug fix, refactor, styling, etc.)
|
|
33
|
+
- Key implementation details and patterns
|
|
34
|
+
- Potential impacts or side effects
|
|
35
|
+
- Testable and user-facing changes
|
|
36
|
+
|
|
37
|
+
### Statamic-Specific Analysis Guide
|
|
38
|
+
|
|
39
|
+
| File Type | What to Look For | Testing Implications |
|
|
40
|
+
| --------- | ---------------- | -------------------- |
|
|
41
|
+
| `resources/views/**/*.antlers.html` | Template conditionals, partial usage, cascade values, collection/global references | Tests cover each affected page type and content state |
|
|
42
|
+
| `resources/fieldsets/*.yaml`, `resources/blueprints/**/*.yaml` | New fields, validation rules, display conditions, defaults | Render tests confirm content |
|
|
43
|
+
| `config/*.php`, `routes/*.php` | Environment-sensitive behavior, routing changes, feature flags | Test affected routes and relevant config-dependent scenarios |
|
|
44
|
+
| `app/**/*.php` | Controllers, listeners, view models, tags, commands, queries | Exercise the feature path and likely failure states |
|
|
45
|
+
| `resources/js/**/*`, `resources/css/**/*`, `vite.config.*` | Interactive behavior, asset bundling, responsive styling | Test interactions, breakpoints |
|
|
46
|
+
| `lang/**/*`, `resources/lang/**/*` | New translation keys or copy changes | Verify translated strings appear correctly in each locale |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Step 3: Generate PR Content
|
|
51
|
+
|
|
52
|
+
Based on the diff, generate:
|
|
53
|
+
|
|
54
|
+
### PR Title
|
|
55
|
+
|
|
56
|
+
Concise and descriptive. Use conventional commit style where appropriate (`feat:`, `fix:`, `refactor:`, `chore:`).
|
|
57
|
+
|
|
58
|
+
### PR Summary
|
|
59
|
+
|
|
60
|
+
1–2 sentences in plain, non-technical language describing what the changes accomplish from a user/business perspective.
|
|
61
|
+
|
|
62
|
+
### Approach
|
|
63
|
+
|
|
64
|
+
Technical description covering:
|
|
65
|
+
|
|
66
|
+
- Key files modified and why
|
|
67
|
+
- Implementation strategy
|
|
68
|
+
- Notable patterns or techniques
|
|
69
|
+
- Trade-offs or decisions made
|
|
70
|
+
|
|
71
|
+
### Testing Steps
|
|
72
|
+
|
|
73
|
+
Generate comprehensive, grouped testing steps as checkboxes. Cover all relevant categories:
|
|
74
|
+
|
|
75
|
+
**Functional Testing**
|
|
76
|
+
|
|
77
|
+
- New features or modified behavior
|
|
78
|
+
- Form inputs, buttons, interactive elements
|
|
79
|
+
- Conditional logic and different states
|
|
80
|
+
|
|
81
|
+
**Visual/UI Testing**
|
|
82
|
+
|
|
83
|
+
- Layout across breakpoints (mobile, tablet, desktop)
|
|
84
|
+
- New or modified CSS
|
|
85
|
+
- Template output across affected entry types and content states
|
|
86
|
+
- Hover states, transitions, animations
|
|
87
|
+
|
|
88
|
+
**Control Panel Testing**
|
|
89
|
+
|
|
90
|
+
- New blueprint or fieldset fields
|
|
91
|
+
- Validation rules and default values
|
|
92
|
+
- Replicator/grid/set add/remove/reorder behavior
|
|
93
|
+
|
|
94
|
+
**Edge Cases**
|
|
95
|
+
|
|
96
|
+
- Empty states (no content, no images)
|
|
97
|
+
- Maximum content (long text, many items)
|
|
98
|
+
- Missing or broken assets
|
|
99
|
+
|
|
100
|
+
**Accessibility**
|
|
101
|
+
|
|
102
|
+
- Keyboard navigation
|
|
103
|
+
- Screen reader compatibility
|
|
104
|
+
- Focus states and tab order
|
|
105
|
+
- Color contrast
|
|
106
|
+
|
|
107
|
+
**Browser/Device**
|
|
108
|
+
|
|
109
|
+
- Cross-browser compatibility
|
|
110
|
+
- Mobile touch interactions
|
|
111
|
+
|
|
112
|
+
Format example:
|
|
113
|
+
|
|
114
|
+
```markdown
|
|
115
|
+
**Feature Functionality**
|
|
116
|
+
|
|
117
|
+
- [ ] Verify the "Show price" toggle appears in the relevant blueprint in the Control Panel
|
|
118
|
+
- [ ] Confirm integration and unit tests pass
|
|
119
|
+
- [ ] Confirm integration and unit tests cover the new feature and any edge cases
|
|
120
|
+
|
|
121
|
+
**Responsive Behavior**
|
|
122
|
+
|
|
123
|
+
- [ ] Test on mobile (< 768px) — items should stack vertically
|
|
124
|
+
- [ ] Test on desktop (≥ 768px) — items should display in a grid
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Step 4: Collect Additional Info
|
|
130
|
+
|
|
131
|
+
Ask the developer for the following (cannot be inferred from code):
|
|
132
|
+
|
|
133
|
+
1. **Basecamp Links** — Links to relevant Basecamp Cards or Todos
|
|
134
|
+
2. **Other Considerations** — Edge cases, known limitations, notes for reviewers
|
|
135
|
+
3. **Demo Links** — Preview URL
|
|
136
|
+
|
|
137
|
+
Then present the generated testing steps and ask:
|
|
138
|
+
|
|
139
|
+
- Any steps to remove (not applicable)?
|
|
140
|
+
- Any additional scenarios to add?
|
|
141
|
+
- Any specific test data or configuration needed?
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Step 5: Review & Confirm
|
|
146
|
+
|
|
147
|
+
Present the complete PR description to the developer for final review. Ask them to confirm it's accurate or request changes.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Step 6: Create the Pull Request
|
|
152
|
+
|
|
153
|
+
Once confirmed, create the PR with GitHub CLI:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
gh pr create \
|
|
157
|
+
--base "{target_branch}" \
|
|
158
|
+
--head "{source_branch}" \
|
|
159
|
+
--title "{auto_generated_title}" \
|
|
160
|
+
--body "{generated_pr_body}"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **On success**: Share the PR URL with the developer.
|
|
164
|
+
- **On failure**: Display the error and troubleshoot:
|
|
165
|
+
- Not authenticated → `gh auth status` / `gh auth login`
|
|
166
|
+
- Branch not pushed → `git push -u origin {source_branch}`
|
|
167
|
+
- No commits between branches → inform developer
|
|
168
|
+
- PR already exists → provide link to existing PR
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## PR Body Template
|
|
173
|
+
|
|
174
|
+
Use this exact format for the `--body` parameter:
|
|
175
|
+
|
|
176
|
+
```markdown
|
|
177
|
+
### PR Summary:
|
|
178
|
+
|
|
179
|
+
{auto_generated_summary}
|
|
180
|
+
|
|
181
|
+
### Tasks Included?
|
|
182
|
+
|
|
183
|
+
{basecamp_links}
|
|
184
|
+
|
|
185
|
+
### What approach did you take?
|
|
186
|
+
|
|
187
|
+
{auto_generated_approach}
|
|
188
|
+
|
|
189
|
+
### Other considerations
|
|
190
|
+
|
|
191
|
+
{considerations}
|
|
192
|
+
|
|
193
|
+
### Testing steps/scenarios
|
|
194
|
+
|
|
195
|
+
{auto_generated_and_reviewed_testing_steps_as_checkboxes}
|
|
196
|
+
|
|
197
|
+
### Demo links
|
|
198
|
+
|
|
199
|
+
- [Site]({site_url})
|
|
200
|
+
- [Editor]({site_url/cp})
|
|
201
|
+
|
|
202
|
+
### Additional Information
|
|
203
|
+
|
|
204
|
+
{additional_info}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Workflow Summary
|
|
210
|
+
|
|
211
|
+
1. Ask for source and target branches
|
|
212
|
+
2. Fetch and analyze the diff
|
|
213
|
+
3. Generate title, summary, approach, and testing steps
|
|
214
|
+
4. Collect Basecamp links, demo URLs, and other info; review testing steps
|
|
215
|
+
5. Present complete PR for final confirmation
|
|
216
|
+
6. Create the PR with `gh pr create` and share the URL
|