@gallopsystems/agent-skills 1.28.0 → 1.30.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 +1 -1
- package/plugins/doctl/skills/doctl/SKILL.md +19 -1
- package/plugins/git-github/skills/git-github/stacked-prs.md +3 -1
- package/plugins/linear/.claude-plugin/plugin.json +1 -1
- package/plugins/linear/skills/linear/SKILL.md +136 -862
- package/plugins/linear/skills/linear/cli.md +249 -0
- package/plugins/linear/skills/linear/examples/backend-data-model.md +31 -0
- package/plugins/linear/skills/linear/examples/backend-feature.md +30 -0
- package/plugins/linear/skills/linear/examples/bug.md +30 -0
- package/plugins/linear/skills/linear/examples/discovery.md +34 -0
- package/plugins/linear/skills/linear/examples/frontend-feature.md +36 -0
- package/plugins/linear/skills/linear/examples/right-and-wrong.md +138 -0
- package/plugins/linear/skills/linear/examples/small-improvement.md +24 -0
- package/plugins/linear/skills/linear/tech-stack.md +1 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# `linear.mjs` — CLI Fallback
|
|
2
|
+
|
|
3
|
+
Read this only when you need an operation the Linear MCP server doesn't expose (see **Linear Tooling** in `SKILL.md`). The workflow rules in `SKILL.md` — status, placement, labels, templates — apply no matter which tool you use.
|
|
4
|
+
|
|
5
|
+
The fallback tooling is a single zero-dependency Node script, `bin/linear.mjs`. It runs on bare `node` (v18.3+ — no `npm install`, no `tsx`, no build step) and uses symbolic names instead of raw UUIDs (`--state backlog`, `--assignee frontend`, `--labels bug,frontend`).
|
|
6
|
+
|
|
7
|
+
Invoke it as `node <skill>/bin/linear.mjs <command> [args] [--flags]`. Examples below write `node linear.mjs` for brevity — use the full path to the file, or `cd` into the skill's `bin/` directory first. Run `node linear.mjs help` for the full command list.
|
|
8
|
+
|
|
9
|
+
It wraps the Linear GraphQL API and reads `LINEAR_API_KEY` from the environment plus the workspace config from `~/.config/linctl/workspace.json`. There is nothing to source — every invocation loads config fresh.
|
|
10
|
+
|
|
11
|
+
## Setup (run once per user)
|
|
12
|
+
|
|
13
|
+
### Workspace bootstrap config
|
|
14
|
+
|
|
15
|
+
Before running any `linear.mjs` command, verify that the per-user workspace config exists at `~/.config/linctl/workspace.json` (override path with `$LINCTL_WORKSPACE_FILE`). This file holds **every team** in the workspace (each with its own UUID plus its workflow-state and label UUIDs — states differ per team), an optional `defaultTeam`, and the Linear member UUIDs that play the Frontend/PM and Backend roles. Without it, every command that needs the team, members, states, or labels fails.
|
|
16
|
+
|
|
17
|
+
> **Multi-team workspaces:** `workspace.json` registers all teams, but `states`/`labels` are per-team (each team's `Todo` is a distinct UUID). Which team a command targets is resolved in this order: the **`--team <key|name|uuid>`** flag → the **`LINCTL_DEFAULT_TEAM`** env var (a per-repo default — set it via direnv/`.envrc` or your shell so every command in a repo targets that team) → the **`defaultTeam`** field in `workspace.json`. If none resolve, `--team` is **required** on team-scoped commands; workspace-wide commands (e.g. `list-initiatives`) work without a team. A legacy config (predating per-team support, i.e. with no `defaultTeam` key and top-level `states`/`labels`) still works — it falls back to the first registered team — but re-run `init` to migrate it to the per-team schema.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
[ -f "${LINCTL_WORKSPACE_FILE:-$HOME/.config/linctl/workspace.json}" ] && echo "ok" || echo "missing"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**If missing,** instruct the user to run:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
node linear.mjs init
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`init` calls Linear's GraphQL API, lists the workspace's members, and prompts the user to designate (1) the Frontend/PM lead and (2) the Backend lead by number, then (3) an optional default team key (blank = no default, so `--team` is required on each team-scoped call). It registers **all** teams with their per-team states/labels and writes `~/.config/linctl/workspace.json`. The config is read fresh on every invocation — no re-sourcing needed.
|
|
30
|
+
|
|
31
|
+
### `LINEAR_API_KEY`
|
|
32
|
+
|
|
33
|
+
`linear.mjs` reads `LINEAR_API_KEY` from the environment. Before using any command, check whether it's set:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
[ -n "$LINEAR_API_KEY" ] && echo "set" || echo "missing"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**If missing, onboard the user:**
|
|
40
|
+
|
|
41
|
+
1. Tell them: *"I need a Linear personal API key to run the CLI. Create one at https://linear.app/settings/account/security (click 'New API key', name it 'Claude Code', copy the `lin_api_...` token), then paste it here in chat."*
|
|
42
|
+
2. When they paste the key, install it into `~/.zshenv` so every future shell — including the ones Claude Code spawns — picks it up automatically:
|
|
43
|
+
```bash
|
|
44
|
+
echo 'export LINEAR_API_KEY=lin_api_THEIR_KEY_HERE' >> ~/.zshenv
|
|
45
|
+
```
|
|
46
|
+
(Use `~/.bashrc` instead if the user is on bash.)
|
|
47
|
+
3. Export it in the current shell too so the next tool call works without restart:
|
|
48
|
+
```bash
|
|
49
|
+
export LINEAR_API_KEY=lin_api_THEIR_KEY_HERE
|
|
50
|
+
```
|
|
51
|
+
4. Verify with a harmless call: `node linear.mjs list-members`.
|
|
52
|
+
|
|
53
|
+
**Never commit the key, never write it into `.env` or any project file** — `~/.zshenv` is the single source of truth.
|
|
54
|
+
|
|
55
|
+
## Symbolic names (no UUIDs needed)
|
|
56
|
+
|
|
57
|
+
The CLI resolves friendly names against `workspace.json`, so you rarely need raw UUIDs:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
--team ACME | "Acme Corp" (team key or name) (or a UUID)
|
|
61
|
+
--state todo | backlog | "in progress" | "in review" | done | canceled (or a UUID)
|
|
62
|
+
--assignee frontend | backend (or a UUID)
|
|
63
|
+
--labels bug,frontend,feature (comma-separated label names) (or UUIDs)
|
|
64
|
+
--priority 0-4 or none | urgent | high | medium | low
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`--team` selects which team a team-scoped command runs against, and `--state`/`--labels` then resolve against **that team's** states and labels. When `--team` is omitted it falls back to `$LINCTL_DEFAULT_TEAM` (a per-repo default) and then to `workspace.json`'s `defaultTeam`; if none are set you'll get an error listing the registered team keys. Workspace-wide commands (initiatives) don't need a team. Any value that's already a UUID is passed through untouched. Label names match case-insensitively with `-` and space interchangeable (`tech-debt` → `Tech Debt`), but `init` only registers the type and domain labels (`discovery`, `tech-debt`, `bug`, `feature`, `improvement`, `frontend`, `backend`, `db`) — apply the status flags (`client-request`, `Needs Clarification`, `agent blocked`) via the MCP server or by UUID. Project and milestone IDs are still UUIDs (pass them with `--project` / `--milestone`).
|
|
68
|
+
|
|
69
|
+
## Creating Issues
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# --project and --milestone are always required
|
|
73
|
+
node linear.mjs create-issue \
|
|
74
|
+
--title 'Add user profile page' \
|
|
75
|
+
--description 'Create /profile page with user info and settings' \
|
|
76
|
+
--priority high \
|
|
77
|
+
--state backlog \
|
|
78
|
+
--assignee frontend \
|
|
79
|
+
--labels feature,frontend \
|
|
80
|
+
--estimate 3 \
|
|
81
|
+
--project 'project-uuid-here' \
|
|
82
|
+
--milestone 'milestone-uuid-here'
|
|
83
|
+
|
|
84
|
+
# Create a bug report
|
|
85
|
+
node linear.mjs create-issue \
|
|
86
|
+
--title 'Login redirect fails on Safari' \
|
|
87
|
+
--description 'Users on Safari not redirected after login. Reproduced on Safari 17.' \
|
|
88
|
+
--priority urgent \
|
|
89
|
+
--state backlog \
|
|
90
|
+
--assignee frontend \
|
|
91
|
+
--labels bug,frontend \
|
|
92
|
+
--project 'project-uuid-here' \
|
|
93
|
+
--milestone 'milestone-uuid-here'
|
|
94
|
+
|
|
95
|
+
# Long descriptions: pass a file instead of inline text (no shell-escaping)
|
|
96
|
+
node linear.mjs create-issue --title 'Investigate perf issue' --state backlog \
|
|
97
|
+
--description-file ./issue-body.md \
|
|
98
|
+
--project 'project-uuid' --milestone 'milestone-uuid'
|
|
99
|
+
|
|
100
|
+
# Find the existing M project for the milestone this work falls under — do not create one.
|
|
101
|
+
# (Prefer the MCP `get_initiative` with includeProjects; this lists them via the CLI.)
|
|
102
|
+
node linear.mjs list-projects # copy the [KEY] M<n> project's UUID
|
|
103
|
+
PROJECT_ID='project-uuid-here'
|
|
104
|
+
# Find the deliverable milestone the work falls under — do not create one in an M project.
|
|
105
|
+
node linear.mjs list-milestones "$PROJECT_ID" # copy the milestone's UUID
|
|
106
|
+
MILESTONE_ID='milestone-uuid-here'
|
|
107
|
+
node linear.mjs create-issue --title 'Investigate performance issue' --state backlog \
|
|
108
|
+
--project "$PROJECT_ID" --milestone "$MILESTONE_ID"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Priority Values
|
|
112
|
+
- `0` = No priority
|
|
113
|
+
- `1` = Urgent
|
|
114
|
+
- `2` = High
|
|
115
|
+
- `3` = Medium
|
|
116
|
+
- `4` = Low
|
|
117
|
+
|
|
118
|
+
## Listing & Filtering Issues
|
|
119
|
+
```bash
|
|
120
|
+
# List all issues (pretty table by default)
|
|
121
|
+
node linear.mjs list-issues
|
|
122
|
+
|
|
123
|
+
# Filter by state type: backlog, unstarted, started, completed, canceled
|
|
124
|
+
node linear.mjs list-issues started
|
|
125
|
+
|
|
126
|
+
# Raw JSON output (for piping) — add --json to any list command
|
|
127
|
+
node linear.mjs list-issues --json
|
|
128
|
+
node linear.mjs list-issues started --json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Updating Issues
|
|
132
|
+
```bash
|
|
133
|
+
# Move issue by status name
|
|
134
|
+
node linear.mjs move-issue "issue-uuid" "In Progress"
|
|
135
|
+
node linear.mjs move-issue "issue-uuid" "Done"
|
|
136
|
+
|
|
137
|
+
# Assign to a team member by role
|
|
138
|
+
node linear.mjs assign-issue "issue-uuid" frontend
|
|
139
|
+
node linear.mjs assign-issue "issue-uuid" backend
|
|
140
|
+
|
|
141
|
+
# General update — symbolic flags
|
|
142
|
+
node linear.mjs update-issue "issue-uuid" --priority urgent --state todo
|
|
143
|
+
|
|
144
|
+
# Or merge arbitrary raw JSON input with --raw
|
|
145
|
+
node linear.mjs update-issue "issue-uuid" --raw '{"priority":1}'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Issue Dependencies
|
|
149
|
+
|
|
150
|
+
Prefer the MCP relations (see `SKILL.md`); these are the fallback:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# Create a "blocks" dependency (backend blocks frontend)
|
|
154
|
+
node linear.mjs add-dependency "$BLOCKER_ISSUE_ID" "$BLOCKED_ISSUE_ID"
|
|
155
|
+
|
|
156
|
+
# List all dependencies for an issue (both directions)
|
|
157
|
+
node linear.mjs list-dependencies "$ISSUE_ID"
|
|
158
|
+
|
|
159
|
+
# Remove a dependency by relation UUID (get UUID from list-dependencies)
|
|
160
|
+
node linear.mjs remove-dependency "$RELATION_ID"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Comments
|
|
164
|
+
```bash
|
|
165
|
+
# Add a comment to an issue
|
|
166
|
+
node linear.mjs add-comment "$ISSUE_ID" --body "Comment body text here"
|
|
167
|
+
|
|
168
|
+
# Long comment from a file (no shell-escaping)
|
|
169
|
+
node linear.mjs add-comment "$ISSUE_ID" --body-file ./comment.md
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Searching
|
|
173
|
+
```bash
|
|
174
|
+
node linear.mjs search-issues "login bug"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Projects & Milestones
|
|
178
|
+
```bash
|
|
179
|
+
# Create a new project (linked to an initiative)
|
|
180
|
+
# Projects mirror the signed proposal's milestones ([KEY] M<n>) or a confirmed revision ([KEY] R<n>).
|
|
181
|
+
# Never create one to hold work you couldn't place — see "Never invent a project".
|
|
182
|
+
node linear.mjs create-project --name "[KEY] M1 — Milestone Name" --initiative "$INITIATIVE_ID" --description "Short description"
|
|
183
|
+
|
|
184
|
+
# List all projects (pretty table with initiative, state, progress)
|
|
185
|
+
node linear.mjs list-projects
|
|
186
|
+
|
|
187
|
+
# List milestones within a project
|
|
188
|
+
node linear.mjs list-milestones "$PROJECT_ID"
|
|
189
|
+
|
|
190
|
+
# List issues grouped by milestone within a project
|
|
191
|
+
node linear.mjs list-project-issues "$PROJECT_ID"
|
|
192
|
+
|
|
193
|
+
# Raw JSON variants (for piping) — add --json
|
|
194
|
+
node linear.mjs list-projects --json
|
|
195
|
+
node linear.mjs list-milestones "$PROJECT_ID" --json
|
|
196
|
+
node linear.mjs list-project-issues "$PROJECT_ID" --limit 200 --json
|
|
197
|
+
|
|
198
|
+
# Only inside a confirmed R project: create a milestone for one of the revision's deliverables
|
|
199
|
+
MILESTONE_ID="$(node linear.mjs create-milestone "$R_PROJECT_ID" "Deliverable name" | node -e "process.stdin.once('data',d=>console.log(JSON.parse(d).data.projectMilestoneCreate.projectMilestone.id))")"
|
|
200
|
+
|
|
201
|
+
# Creating a project is only for a CONFIRMED out-of-scope revision — next R<n>, same initiative
|
|
202
|
+
node linear.mjs create-project --name "[KEY] R1 — Revision Name" --initiative "$INITIATIVE_ID" --description "Short description"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Bulk Moves (MCP has no equivalent)
|
|
206
|
+
```bash
|
|
207
|
+
# Move many issues to a milestone — 0.5s delay between calls so Linear doesn't drop writes.
|
|
208
|
+
# Keep batches to ~9 issues and verify (list-project-issues) between batches.
|
|
209
|
+
node linear.mjs batch-move-to-milestone "$MILESTONE_ID" "$ISSUE_ID_1" "$ISSUE_ID_2" "$ISSUE_ID_3"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Raw GraphQL
|
|
213
|
+
```bash
|
|
214
|
+
# Escape hatch for anything else, e.g. a field save_issue didn't apply
|
|
215
|
+
node linear.mjs api 'mutation($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success } }' \
|
|
216
|
+
'{"id": "issue-uuid", "input": {"projectMilestoneId": "milestone-uuid"}}'
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Initiatives
|
|
220
|
+
```bash
|
|
221
|
+
# Create a new initiative (= a newly signed proposal; a repeat client gets another one)
|
|
222
|
+
node linear.mjs create-initiative --name "ClientName" --description "Short description"
|
|
223
|
+
|
|
224
|
+
# List all initiatives (pretty table with ID, status, description)
|
|
225
|
+
node linear.mjs list-initiatives
|
|
226
|
+
|
|
227
|
+
# Get full initiative detail by name (case-insensitive)
|
|
228
|
+
node linear.mjs get-initiative-by-name "Northwind"
|
|
229
|
+
|
|
230
|
+
# Get full initiative detail by ID
|
|
231
|
+
node linear.mjs get-initiative "$INITIATIVE_ID"
|
|
232
|
+
|
|
233
|
+
# Update initiative content (markdown) or description
|
|
234
|
+
node linear.mjs update-initiative "$INITIATIVE_ID" --content-file ./initiative-notes.md
|
|
235
|
+
node linear.mjs update-initiative "$INITIATIVE_ID" --description "Short description"
|
|
236
|
+
|
|
237
|
+
# Add an external link (e.g., repo) as a resource on the initiative
|
|
238
|
+
node linear.mjs add-initiative-link "$INITIATIVE_ID" "https://github.com/org/repo" "GitHub Repo"
|
|
239
|
+
|
|
240
|
+
# Raw JSON of all initiatives
|
|
241
|
+
node linear.mjs list-initiatives --json
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Info Commands
|
|
245
|
+
```bash
|
|
246
|
+
node linear.mjs list-states # Workflow states
|
|
247
|
+
node linear.mjs list-members # Team members
|
|
248
|
+
node linear.mjs list-labels # Labels
|
|
249
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Title: Store the technician name and certification date on the visit
|
|
3
|
+
Labels: Feature, Backend, DB
|
|
4
|
+
Estimate: M (3)
|
|
5
|
+
Note: Existing columns are named because they go away; nothing new is prescribed —
|
|
6
|
+
no column list, types, or indexes. The Backend lead designs the schema.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
**Context:** [Our thinking — Why certification moves off the technician](https://linear.app/<workspace>/document/<our-thinking-doc>) · [How they operate — Where certification dates come from](https://linear.app/<workspace>/document/<how-they-operate-doc>)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
The customer keeps technician certification dates in their HR system and doesn't want to maintain technician records in two places. They've asked to just enter the technician's name and certification date on each visit, instead of picking from a technician list.
|
|
16
|
+
|
|
17
|
+
## Functionality
|
|
18
|
+
|
|
19
|
+
* Store the technician's name on the visit as free text. It's optional — a visit without a name is an unassigned slot (what we call a slot visit today).
|
|
20
|
+
* Store a certification date on the visit, entered by the user. Use it to work out the rate bracket and whether supervision is required, as of the job's scheduling date.
|
|
21
|
+
* If the job type requires certification and the visit has no date, flag it the same way we flag an uncertified technician today. Don't guess.
|
|
22
|
+
* Remove `visits.technician_id` and the technician/slot visit kinds. Every visit has the same shape, with an optional name.
|
|
23
|
+
* This is greenfield, so there's no migration of existing technician visits. Update the seeds to use named visits.
|
|
24
|
+
|
|
25
|
+
## Acceptance criteria
|
|
26
|
+
|
|
27
|
+
- [ ] A visit can be saved with a name and a date, with just a name, or with neither
|
|
28
|
+
- [ ] Rates and supervision are calculated from the visit's certification date
|
|
29
|
+
- [ ] Saving the schedule keeps the name and date on every visit
|
|
30
|
+
- [ ] Table and column comments explain the new columns and what they replace
|
|
31
|
+
- [ ] Tests written
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Title: Send a quote by email from the platform
|
|
3
|
+
Labels: Feature, Backend
|
|
4
|
+
Estimate: M (3)
|
|
5
|
+
Relation: KEY-141 (the sending-address decision) is linked inline, not narrated
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
**Context:** [Our thinking — Recipients and template](https://linear.app/<workspace>/document/<our-thinking-doc>) · [How they operate — Customer contacts](https://linear.app/<workspace>/document/<how-they-operate-doc>)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Context
|
|
13
|
+
|
|
14
|
+
Dispatchers say getting a quote out to a customer takes too long. Right now they generate it, download it, and email it from Outlook. They want to send it without leaving the platform.
|
|
15
|
+
|
|
16
|
+
## Functionality
|
|
17
|
+
|
|
18
|
+
* Send an existing quote to one or more recipients, with the quote PDF attached (the one from `/api/jobs/:id/quotes/:quoteId/pdf`).
|
|
19
|
+
* Recipients have to be contacts of the job's customer (`contacts` rows with that `customer_id`). Contacts without an email address can't be picked.
|
|
20
|
+
* Send from the logged-in user's own email address, as decided in KEY-141.
|
|
21
|
+
* The frontend sends the subject and body — it fills in a default from a template, and the user can edit it. The backend sends the subject and body exactly as submitted and doesn't replace the body.
|
|
22
|
+
* Reject the send if any recipient isn't a contact of the job's customer.
|
|
23
|
+
* We're not keeping a record of sends for now — what was sent, to whom, when, or whether it was delivered. That's out of scope for this revision.
|
|
24
|
+
|
|
25
|
+
## Acceptance criteria
|
|
26
|
+
|
|
27
|
+
- [ ] Each valid recipient gets the quote, sent from the user's own address
|
|
28
|
+
- [ ] Sending to someone who isn't a contact of the job's customer fails
|
|
29
|
+
- [ ] Sending to a contact with no email address fails
|
|
30
|
+
- [ ] Tests written
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Title: Invoices list total doesn't include credits
|
|
3
|
+
Labels: Bug, Backend
|
|
4
|
+
Estimate: S (2)
|
|
5
|
+
Note: The title states the broken behavior — no "Fix:" prefix. Root cause and location
|
|
6
|
+
come from reading the code on main, not from the reporter's guess.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
## Bug
|
|
10
|
+
|
|
11
|
+
The invoices list shows the total before credits. So an invoice for $1,000 with a $200 credit shows $1,000 in the list but $800 on the invoice page. The invoice page is right.
|
|
12
|
+
|
|
13
|
+
## Root cause
|
|
14
|
+
|
|
15
|
+
The list endpoint adds up `invoice_lines.amount` directly. The detail endpoint uses `getInvoiceTotals`, which takes the credits off.
|
|
16
|
+
|
|
17
|
+
## Steps to reproduce
|
|
18
|
+
|
|
19
|
+
1. Apply a credit to any open invoice.
|
|
20
|
+
2. Open the invoices list.
|
|
21
|
+
|
|
22
|
+
## Likely location
|
|
23
|
+
|
|
24
|
+
* `server/api/invoices/index.get.ts` (the list total)
|
|
25
|
+
* `server/utils/invoice-totals.ts` (`getInvoiceTotals`, which does it correctly)
|
|
26
|
+
|
|
27
|
+
## Acceptance criteria
|
|
28
|
+
|
|
29
|
+
- [ ] The list and the invoice page show the same total, with or without a credit
|
|
30
|
+
- [ ] Tests written
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Title: Decide what hours to check overtime thresholds against
|
|
3
|
+
Labels: Discovery, Backend
|
|
4
|
+
Estimate: S (2)
|
|
5
|
+
Note: This is an open question only because the requester said it was one — the agent
|
|
6
|
+
never decides on its own that something needs the client. Each option includes
|
|
7
|
+
its trade-offs; the answer gets folded into the dependent issues, not logged here.
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
**Context:** [Our thinking — Overtime thresholds](https://linear.app/<workspace>/document/<our-thinking-doc>) · [How they operate — How the system works today](https://linear.app/<workspace>/document/<how-they-operate-doc>)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
Overtime has to keep working once technician records go away, and it depends on a number that's only stored on those records.
|
|
17
|
+
|
|
18
|
+
A job type's rate item can have a `min_weekly_hours` threshold. Below it, the base rate applies; above it, the extra hours are paid at the overtime rate.
|
|
19
|
+
|
|
20
|
+
Right now we check the threshold against `technicians.weekly_hours`, which is the technician's **total** week across all their jobs. That's different on purpose from `visits.hours`, which is just the hours on one visit — the scheduler shows a warning when they don't match (KEY-92), since a technician who works for two customers has more hours in total than on either visit.
|
|
21
|
+
|
|
22
|
+
## Open question
|
|
23
|
+
|
|
24
|
+
What should we compare the threshold against instead?
|
|
25
|
+
|
|
26
|
+
* **The visit's own `hours`.** It's already there, so there's nothing new to enter. The downside: a technician split across two customers gets judged on each share separately, so they might miss a threshold they actually meet.
|
|
27
|
+
* **A second hours field on the visit.** Keeps today's distinction — say, "40 hours a week in total" next to the 20 hours on this visit — but it's one more field to fill in.
|
|
28
|
+
|
|
29
|
+
If the customer never actually splits a technician across customers, the first option is the answer.
|
|
30
|
+
|
|
31
|
+
## Acceptance criteria
|
|
32
|
+
|
|
33
|
+
- [ ] We have an answer from the customer, and it's written up in the project's "Our thinking" doc
|
|
34
|
+
- [ ] The issues that depend on this are updated with the answer
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Title: Pick the job type on the job and enter the technician name and date on each visit
|
|
3
|
+
Labels: Feature, Frontend
|
|
4
|
+
Estimate: L (5)
|
|
5
|
+
Note: Every path in UI Notes was verified in the repo on main. If the repo can't be
|
|
6
|
+
checked, leave paths out entirely — no "To Determine" / TBD section.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
**Context:** [Our thinking — What that means](https://linear.app/<workspace>/document/<our-thinking-doc>) · [How they operate — How the system works today](https://linear.app/<workspace>/document/<how-they-operate-doc>)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
Dispatchers set the same job type on every visit of a job, which is repetitive and easy to get wrong. They've also asked to stop picking technicians from a list here — they keep that list in their HR system. They want to pick the job type once for the job, and type the technician's name and certification date on each visit.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- [ ] The job editor lets you pick one job type (or none) next to the service category. No job type means no restrictions.
|
|
20
|
+
- [ ] Each visit has a free-text technician name. If it's blank, the visit is an unassigned slot, so the technician/slot toggle goes away.
|
|
21
|
+
- [ ] Each visit has a certification date, with a date picker.
|
|
22
|
+
- [ ] The visit shows the rate that date gives. If the job type needs a date and there isn't one, show that clearly instead of showing $0.
|
|
23
|
+
- [ ] Remove technician search/select from the scheduler.
|
|
24
|
+
|
|
25
|
+
## UI Notes
|
|
26
|
+
|
|
27
|
+
* Components: `app/components/JobEditor.vue` (job type picker), `app/components/VisitRow.vue`, `app/components/VisitRatePreview.vue` — checked in the repo.
|
|
28
|
+
* Pages: `app/pages/jobs/[id]/edit.vue`, `app/pages/jobs/[id]/index.vue`.
|
|
29
|
+
* VoltSelect for the job type, VoltInputText for the name, VoltDatePicker for the date.
|
|
30
|
+
* Follow DESIGN_LANGUAGE.md — zinc palette, no decorative shadows.
|
|
31
|
+
|
|
32
|
+
## Acceptance criteria
|
|
33
|
+
|
|
34
|
+
- [ ] You can schedule a job from start to finish with a job type and a mix of named and unnamed visits, and the pricing comes out right
|
|
35
|
+
- [ ] Nothing in the scheduler references technician records anymore
|
|
36
|
+
- [ ] Tests written
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Issue Writing — Right and Wrong
|
|
2
|
+
|
|
3
|
+
Each pair is a real correction made on a drafted issue. ❌ is what agents tend to write; ✅ is how we write it.
|
|
4
|
+
|
|
5
|
+
## Titles
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
❌ Fix: Invoice list total wrong
|
|
9
|
+
❌ [ACME] UI: Add quote email
|
|
10
|
+
❌ Spike: overtime hours
|
|
11
|
+
✅ Invoices list total doesn't include credits
|
|
12
|
+
✅ Send a quote by email from the platform
|
|
13
|
+
✅ Decide what hours to check overtime thresholds against
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
No prefixes at all — no client, no domain, no `Fix:` / `Chore:` / `Spike:`. The team already identifies the client, and the labels show the type and domain. Just say what the issue is about.
|
|
17
|
+
|
|
18
|
+
## Framing
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
❌ ## Context
|
|
22
|
+
Add a Send button to the quote page that emails the PDF through Resend.
|
|
23
|
+
|
|
24
|
+
❌ ## Context
|
|
25
|
+
Dispatchers want to email quotes from the platform, which will improve
|
|
26
|
+
customer satisfaction and reduce churn.
|
|
27
|
+
|
|
28
|
+
❌ ## Context
|
|
29
|
+
There's nowhere in the app to attach a quote to an email, so dispatchers
|
|
30
|
+
print each quote and fax it to the customer.
|
|
31
|
+
|
|
32
|
+
✅ ## Context
|
|
33
|
+
Dispatchers say getting a quote out to a customer takes too long. Right now
|
|
34
|
+
they generate it, download it, and email it from Outlook. They want to send
|
|
35
|
+
it without leaving the platform.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Frame the issue around the intent — who has the problem and what they're trying to get done — not the fix. The first ❌ goes straight to the solution. The second is framed around intent, but it includes a reason nobody gave. The third makes up a workaround: nobody said anything about faxing, the agent guessed it because the feature doesn't exist. The reason and the current workaround both come from the requester. The ✅ mentions Outlook because the dispatchers said that's what they do. If the requester didn't tell you, ask them.
|
|
39
|
+
|
|
40
|
+
## Describing things literally
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
❌ The generated quote PDF is currently silent on tax.
|
|
44
|
+
❌ The list endpoint ignores credits and doesn't know about partial payments.
|
|
45
|
+
|
|
46
|
+
✅ The generated quote PDF doesn't include a tax line.
|
|
47
|
+
✅ The list endpoint doesn't subtract credits or partial payments.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Documents, code, and systems don't speak, know, want, or ignore anything. Say what they do or what they contain.
|
|
51
|
+
|
|
52
|
+
## Backend: behavior, not schema
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
❌ Add a `quote_sends` table:
|
|
56
|
+
- id (bigint, PK)
|
|
57
|
+
- quote_id (bigint, FK → quotes.id, indexed)
|
|
58
|
+
- status (text, CHECK in ('queued','sent','failed'))
|
|
59
|
+
|
|
60
|
+
✅ * Recipients have to be contacts of the job's customer. Contacts without an
|
|
61
|
+
email address can't be picked.
|
|
62
|
+
* We're not keeping a record of sends for now. That's out of scope for this
|
|
63
|
+
revision.
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Say what needs to happen, the business rules, and what's out of scope. The Backend lead decides the tables, columns, types, indexes, and constraints. It's fine to mention columns and endpoints that already exist — just don't design new ones.
|
|
67
|
+
|
|
68
|
+
## Status and enum fields
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
❌ status: `draft`, `sent`, `accepted`, or `expired`
|
|
72
|
+
|
|
73
|
+
✅ We need to be able to tell whether a quote is still being edited, sent but
|
|
74
|
+
not answered yet, accepted, or expired. Only quotes still being edited can
|
|
75
|
+
change, and the jobs list filters on the other three.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Explain what the status needs to tell apart and what depends on it. Let the implementer name the values.
|
|
79
|
+
|
|
80
|
+
## Grounding in the repo
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
❌ Lift the rate math out of `useMockRates.ts` on the wireframe branch.
|
|
84
|
+
❌ Per the plan in KEY-29, rates will come from the job type — that's wrong,
|
|
85
|
+
instead...
|
|
86
|
+
❌ ## To Determine
|
|
87
|
+
- [ ] Which page renders the jobs list?
|
|
88
|
+
|
|
89
|
+
✅ The list endpoint adds up `invoice_lines.amount` directly. The detail
|
|
90
|
+
endpoint uses `getInvoiceTotals`, which takes the credits off.
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Describe what the code on `main` does today, and check it. Don't point at a wireframe branch or a scratch file the assignee might not have — paste what matters into the issue instead. Don't frame the issue around another issue's plan. If you can't check the repo, leave file paths out rather than adding a TBD section.
|
|
94
|
+
|
|
95
|
+
## Links between issues
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
❌ Upstream data capture lives in Scheduling: KEY-14 (captures the visit
|
|
99
|
+
hours). Backend generation is handled by KEY-15 (blocking).
|
|
100
|
+
❌ see the Job Scheduling project
|
|
101
|
+
❌ ## Sub-issues
|
|
102
|
+
- KEY-201
|
|
103
|
+
- KEY-202
|
|
104
|
+
|
|
105
|
+
✅ ...the scheduler shows a warning when they don't match (KEY-92), since a
|
|
106
|
+
technician who works for two customers...
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Link another issue inline, by its identifier, the first time you mention something it covers — once per issue is enough. Dependencies go in Linear relations, not in the text. Linear already shows sub-issues, so don't list them. Anything you name — a project, an issue, a doc — should be a link.
|
|
110
|
+
|
|
111
|
+
## Open questions
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
❌ ## Open questions
|
|
115
|
+
- Should the send be logged? (needs a meeting with the client)
|
|
116
|
+
- Which template should the default body use?
|
|
117
|
+
|
|
118
|
+
✅ (asked the requester before writing — both answers are now in the body)
|
|
119
|
+
* We're not keeping a record of sends for now...
|
|
120
|
+
* The frontend sends the subject and body...
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Ask the requester everything before you write. Only put a question in the issue if the requester tells you it's an open question — don't decide on your own that it needs a meeting.
|
|
124
|
+
|
|
125
|
+
## After a clarifying answer
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
❌ ## Functionality
|
|
129
|
+
* Send to a single recipient.
|
|
130
|
+
...
|
|
131
|
+
## Update (2025-05-01) — clarified with requester
|
|
132
|
+
* Actually, more than one recipient per send.
|
|
133
|
+
|
|
134
|
+
✅ ## Functionality
|
|
135
|
+
* Send an existing quote to one or more recipients...
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The body should always read as the current spec. When an answer changes something, rewrite that part so it reads as if it always said that. No "Update", "Decisions", or "Clarifications" sections, no dated headers, and no struck-through text.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Title: Let app pages use the full window width
|
|
3
|
+
Labels: Improvement, Frontend
|
|
4
|
+
Estimate: XS (1)
|
|
5
|
+
Note: A small, fully-understood change stays small: one requirement, one criterion.
|
|
6
|
+
No doc header — this project has no "Our thinking" / "How they operate" docs.
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
People on wide screens have asked for more room. Every app page is capped at `max-w-6xl` and centered, so tables and the schedule view get squeezed while both sides of the screen stay empty.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- [ ] Remove `max-w-6xl mx-auto` from `PageHeader` and from each page's filter bar and main content wrapper. Keep the side padding as it is.
|
|
16
|
+
|
|
17
|
+
## UI Notes
|
|
18
|
+
|
|
19
|
+
* Component: `app/components/PageHeader.vue`
|
|
20
|
+
* Pages: customers, jobs (index, `[id]`), invoices, schedule, settings, users
|
|
21
|
+
|
|
22
|
+
## Acceptance criteria
|
|
23
|
+
|
|
24
|
+
- [ ] Pages and their headers stretch to the full window width, with the same side padding as before
|
|
@@ -270,4 +270,4 @@ The `NUXT_` prefix auto-binds to `runtimeConfig` in `nuxt.config.ts`.
|
|
|
270
270
|
|------|---------|
|
|
271
271
|
| **pre-commit** | Format (oxfmt) + lint fix (oxlint) on staged `.ts/.vue/.js` files |
|
|
272
272
|
| **pre-push** | Backend tests, frontend tests, typecheck, lint (in parallel) |
|
|
273
|
-
| **post-merge** | Auto `yarn install` if `package.json`/`yarn.lock` changed;
|
|
273
|
+
| **post-merge** | Auto `yarn install` if `package.json`/`yarn.lock` changed; prints a warning if new migrations are detected |
|