@qvac/skills 0.0.0 → 0.1.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.
Files changed (84) hide show
  1. package/LICENSE.md +198 -0
  2. package/README.md +49 -0
  3. package/index.d.ts +4 -0
  4. package/index.js +9 -0
  5. package/package.json +87 -1
  6. package/skills/apple-notes/SKILL.md +92 -0
  7. package/skills/apple-notes/append-note.applescript +9 -0
  8. package/skills/apple-notes/cli.schema.json +32 -0
  9. package/skills/apple-notes/create-note.applescript +15 -0
  10. package/skills/apple-notes/delete-note.applescript +10 -0
  11. package/skills/apple-notes/edit-note.applescript +10 -0
  12. package/skills/apple-notes/read-note.applescript +28 -0
  13. package/skills/apple-notes/references/read.md +65 -0
  14. package/skills/apple-notes/references/write.md +105 -0
  15. package/skills/apple-notes/search-notes.applescript +21 -0
  16. package/skills/apple-reminders/SKILL.md +129 -0
  17. package/skills/apple-reminders/cli.schema.json +201 -0
  18. package/skills/apple-reminders/references/edit.md +69 -0
  19. package/skills/apple-reminders/references/view.md +58 -0
  20. package/skills/asana/SKILL.md +59 -0
  21. package/skills/diagrams/SKILL.md +107 -0
  22. package/skills/diagrams/references/class.md +29 -0
  23. package/skills/diagrams/references/er.md +27 -0
  24. package/skills/diagrams/references/flowchart.md +33 -0
  25. package/skills/diagrams/references/gantt.md +38 -0
  26. package/skills/diagrams/references/mindmap.md +35 -0
  27. package/skills/diagrams/references/pie.md +27 -0
  28. package/skills/diagrams/references/sequence.md +32 -0
  29. package/skills/diagrams/references/state.md +30 -0
  30. package/skills/diagrams/references/timeline.md +28 -0
  31. package/skills/excel/SKILL.md +120 -0
  32. package/skills/excel/references/create.md +374 -0
  33. package/skills/excel/references/edit.md +353 -0
  34. package/skills/excel/references/read.md +99 -0
  35. package/skills/github/SKILL.md +42 -0
  36. package/skills/gmail/SKILL.md +142 -0
  37. package/skills/gmail/operations.json +71 -0
  38. package/skills/google-calendar/SKILL.md +139 -0
  39. package/skills/google-calendar/operations.json +62 -0
  40. package/skills/google-docs/SKILL.md +74 -0
  41. package/skills/google-docs/operations.json +61 -0
  42. package/skills/google-docs/references/create.md +97 -0
  43. package/skills/google-docs/references/edit.md +146 -0
  44. package/skills/google-docs/references/read.md +49 -0
  45. package/skills/google-drive/SKILL.md +118 -0
  46. package/skills/google-drive/operations.json +40 -0
  47. package/skills/google-sheets/SKILL.md +71 -0
  48. package/skills/google-sheets/operations.json +85 -0
  49. package/skills/google-sheets/references/create.md +54 -0
  50. package/skills/google-sheets/references/edit.md +124 -0
  51. package/skills/google-sheets/references/read.md +74 -0
  52. package/skills/image-generation/SKILL.md +48 -0
  53. package/skills/music-generation/SKILL.md +76 -0
  54. package/skills/notion/SKILL.md +61 -0
  55. package/skills/notion/operations.json +53 -0
  56. package/skills/notion/references/comments.md +65 -0
  57. package/skills/notion/references/databases.md +68 -0
  58. package/skills/notion/references/pages.md +119 -0
  59. package/skills/notion/references/tasks.md +28 -0
  60. package/skills/obsidian/SKILL.md +122 -0
  61. package/skills/obsidian/cli.schema.json +392 -0
  62. package/skills/obsidian/references/read.md +79 -0
  63. package/skills/obsidian/references/write.md +67 -0
  64. package/skills/pdf/SKILL.md +110 -0
  65. package/skills/pdf/references/create.md +169 -0
  66. package/skills/pdf/references/transform.md +270 -0
  67. package/skills/pdf/scripts/decrypt.py +26 -0
  68. package/skills/pdf/scripts/encrypt.py +25 -0
  69. package/skills/pdf/scripts/extract_text.py +25 -0
  70. package/skills/pdf/scripts/merge.py +21 -0
  71. package/skills/pdf/scripts/rotate.py +27 -0
  72. package/skills/presentations/SKILL.md +118 -0
  73. package/skills/presentations/references/create.md +399 -0
  74. package/skills/presentations/references/edit.md +314 -0
  75. package/skills/presentations/references/read.md +127 -0
  76. package/skills/spotify/SKILL.md +86 -0
  77. package/skills/weather/SKILL.md +33 -0
  78. package/skills/word/SKILL.md +141 -0
  79. package/skills/word/references/create.md +368 -0
  80. package/skills/word/references/edit.md +704 -0
  81. package/skills/word/references/read.md +141 -0
  82. package/skills/word/references/replace.md +86 -0
  83. package/skills/word/scripts/list_paragraphs.py +19 -0
  84. package/skills/word/scripts/replace_paragraphs.py +58 -0
@@ -0,0 +1,374 @@
1
+ # Creating a Workbook (openpyxl)
2
+
3
+ Create a new `.xlsx` from scratch by running Python through the `exec` tool.
4
+ A new workbook needs **no** `inputs` — do not invent attachment ids. **Exactly
5
+ one** `exec` call per user request when that call succeeds.
6
+
7
+ **A workbook that already exists in this chat is never rebuilt here.** "Add a
8
+ row", "change a cell", "extend the sheet" — any request that starts from an
9
+ existing `.xlsx` is an EDIT: load `references/edit.md` and stage the workbook
10
+ by its `attachmentId`. Building a fresh workbook for an edit request throws
11
+ away everything the user already has.
12
+
13
+ ## The exec call
14
+
15
+ ```json
16
+ {
17
+ "language": "python",
18
+ "packages": ["openpyxl==3.1.5"],
19
+ "outputs": ["report.xlsx"],
20
+ "command": "..."
21
+ }
22
+ ```
23
+
24
+ - `packages` — `["openpyxl==3.1.5"]` on every call. Pin the version; this exact
25
+ version ships with the app and installs with no network; any other version
26
+ has to be downloaded, which fails on a device that is offline. A workbook that
27
+ embeds an image also needs `"pillow"`, **deliberately unpinned** — the runtime owns
28
+ its version and `"pillow==12.2.0"` matches nothing, which sends openpyxl to PyPI
29
+ with it. `["openpyxl==3.1.5", "pillow"]` ships whole and installs offline.
30
+ - `outputs` — `["report.xlsx"]`. `wb.save("report.xlsx")` must match the
31
+ declared output name. `.xlsx` and `.csv` are allowed; `.xlsm` is not. Never
32
+ pass an absolute path to `save()`.
33
+ - `command` — the multi-line Python source, with real newline characters. Never
34
+ collapse it to one line joined by `;` — a `for`/`if`/`with` after a semicolon
35
+ is a `SyntaxError`.
36
+ - No `inputs` key at all — **except** to embed an image, the one file a create
37
+ call stages. See Embedding an Image.
38
+
39
+ ## The Recipe
40
+
41
+ Start from this. It is a complete, working workbook — bold headers, data rows,
42
+ number formats, column widths — saved under the declared output name. Copy it
43
+ and change the content; do not assemble a workbook from memory.
44
+
45
+ **Write the program flat — no `def`, no helper functions, no classes.** Top-level
46
+ statements only, in the order the sample shows. Every real failure of this skill
47
+ has come from a model writing a generator function and then wiring it up wrong: a
48
+ list filled inside a function and read outside it (`NameError: name 'data' is not
49
+ defined`), or a function that was never called, which saves an empty sheet and
50
+ delivers a blank file to the user.
51
+
52
+ **Type the data out as literal rows, however long the series.** Ten years is ten
53
+ tuples — write them. Never build periods with `datetime`/`timedelta` arithmetic
54
+ and never increment a month by hand: `month + 1` past December raises
55
+ `ValueError: month must be in 1..12, not 13`. A year is the plain number `2016`
56
+ and a month is the plain string `"2016-03"` — neither needs a date object.
57
+
58
+ **Give the granularity the user asked for.** "Prices from 2016-2025" is one row
59
+ per year — ten rows. Do not expand it into 120 monthly rows they did not ask for.
60
+
61
+ **Import once, then use that exact name.** After
62
+ `from openpyxl import Workbook` the constructor is `Workbook()` — writing
63
+ `openpyxl.Workbook()` raises `NameError: name 'openpyxl' is not defined`, because
64
+ that import never binds the module. Copy the sample's import block as-is.
65
+
66
+ **Keep every underscore in API names.** `load_workbook`, `create_sheet`,
67
+ `number_format`, `column_dimensions`, `column_letter`, `iter_rows`, `max_row`,
68
+ `merge_cells` — stripping them to `loadworkbook` / `createsheet` fails.
69
+
70
+ **Fill the template in — do not rewrite it.** The block below is the whole
71
+ program. Change only its named slots: `SHEET_TITLE`, `HEADERS`, `ROWS`,
72
+ `MONEY_COLUMNS`, and `OUTPUT` (which must match the declared output). Every other
73
+ line stays character for character. In practice every failed run has come from
74
+ code invented *around* this template — an extra per-row styling loop, a
75
+ hand-built summary row, a second unpack of the same data — not from the template
76
+ itself. If a row needs a number format, put its column letter in
77
+ `MONEY_COLUMNS`; that is the only knob.
78
+
79
+ Nothing goes after `print(...)`, and no comparison, `sorted`, `min`/`max` or
80
+ running total goes between the lines. Values that need working out are worked out
81
+ as you type `ROWS`, and anything the user should notice about the numbers belongs
82
+ in your chat reply, not in more Python. Comparing a year to a label is where the
83
+ last run died: `TypeError: '<=' not supported between instances of 'int' and 'str'`.
84
+
85
+ ```python
86
+ from openpyxl import Workbook
87
+ from openpyxl.styles import Font
88
+
89
+ SHEET_TITLE = "Q1 Sales"
90
+ HEADERS = ["Region", "Units", "Unit Price", "Revenue"]
91
+ ROWS = [ # every tuple already complete, one per row
92
+ ("North", 120, 9.99, 1198.80),
93
+ ("South", 80, 12.50, 1000.00),
94
+ ("East", 200, 7.25, 1450.00),
95
+ ]
96
+ MONEY_COLUMNS = ["C", "D"] # money-formatted columns; [] for none
97
+ OUTPUT = "report.xlsx" # must match the declared output exactly
98
+
99
+ wb = Workbook()
100
+ ws = wb.active # a new workbook already has one sheet
101
+ ws.title = SHEET_TITLE
102
+
103
+ ws.append(HEADERS)
104
+ for cell in ws[1]: # ws[N] is row NUMBER N — never ws[a_list]
105
+ cell.font = Font(bold=True)
106
+ ws.column_dimensions[cell.column_letter].width = 14
107
+ ws.freeze_panes = "A2" # header stays visible while scrolling
108
+
109
+ for row in ROWS: # append whole rows — never unpack them
110
+ ws.append(list(row))
111
+
112
+ for letter in MONEY_COLUMNS:
113
+ for cell in ws[letter][1:]: # ws["C"] is a column; [1:] skips its header
114
+ cell.number_format = '#,##0.00'
115
+
116
+ assert ws.max_row > 1, "no data rows — fix the code, never deliver an empty sheet"
117
+ wb.save(OUTPUT)
118
+ print(f"{len(wb.sheetnames)} sheets, {ws.max_row} rows")
119
+ ```
120
+
121
+ Compute derived columns while writing `ROWS`, not in a loop over them: the
122
+ revenue above is typed into each tuple. Unpacking rows to build other rows
123
+ (`for a, b in ROWS:`) is what raises
124
+ `ValueError: not enough values to unpack` the moment one tuple is a different
125
+ length.
126
+
127
+ Only when the user wants a **live** total, append this after the loop — nothing
128
+ else changes:
129
+
130
+ ```python
131
+ last_data_row = ws.max_row # capture BEFORE appending the total row
132
+ ws.append(["Total", None, None, f"=SUM(D2:D{last_data_row})"])
133
+ ```
134
+
135
+ Capture `ws.max_row` **before** appending a row whose formula refers to the data —
136
+ `max_row` grows with every append, so building the range afterwards produces a
137
+ formula that includes its own cell (a circular reference the user sees as an error
138
+ in Excel). Remember this runtime computes nothing: the formula cell looks empty
139
+ in the chat preview, so say in the reply that the total shows up once the file
140
+ is opened in a spreadsheet app.
141
+
142
+ Every data table gets the three lines the sample shows — bold header row,
143
+ `ws.freeze_panes = "A2"`, and column widths. Without them the user opens a wall
144
+ of `####` columns and loses the header the moment they scroll.
145
+
146
+ When the user asks for made-up, sample, or random data, generate it so it holds
147
+ together: a high is above its close, a low is below it, dates run in order,
148
+ percentages sum to about 100. Drawing each cell independently from a random range
149
+ puts a low of 64,000 next to a high of 21,000 in the same row, and the user reads
150
+ that as a broken file rather than as placeholder data.
151
+
152
+ Number formats are strings on the cell: `'#,##0.00'` for money-style decimals,
153
+ `'0.0%'` for percentages (store the fraction, e.g. `0.31`, not `31`), `'yyyy-mm-dd'`
154
+ for dates.
155
+
156
+ ## Dates
157
+
158
+ Only when the user needs real date sorting or filtering — a year or a month label
159
+ is a plain number or string, and needs none of this. Copy both lines together;
160
+ the import is the half that gets forgotten:
161
+
162
+ ```python
163
+ from datetime import date # NOT `import datetime`, NOT `from datetime import datetime`
164
+
165
+ ws["A2"] = date(2016, 1, 1) # a bare date(...) call — never datetime.date(...)
166
+ ws["A2"].number_format = 'yyyy-mm-dd'
167
+ ```
168
+
169
+ `datetime.date(2016, 1, 1)` after `from datetime import datetime` raises
170
+ `TypeError: descriptor 'date' for 'datetime.datetime' objects doesn't apply to a
171
+ 'int' object`, and with no import at all it raises `NameError`. Both mean the same
172
+ thing: use the two lines above exactly as written.
173
+
174
+ ## Several Sheets
175
+
176
+ When the user asks for N sheets — "a tab per region", "ten sheets of facts" — write
177
+ **one flat list of rows** and let the code group it. Each row names its own sheet as
178
+ its first value, exactly like `ROWS` above, so there are no nested lists to author:
179
+
180
+ ```python
181
+ from openpyxl import Workbook
182
+ from openpyxl.styles import Alignment, Font
183
+
184
+ HEADERS = ["#", "Fact"] # the columns AFTER the sheet name
185
+ WIDTHS = [6, 70] # one width per header column
186
+ ROWS = [ # (sheet, then one value per header column)
187
+ ("Vision", 1, "Parrots see ultraviolet light that humans cannot."),
188
+ ("Vision", 2, "Their color vision uses four cone types, not three."),
189
+ ("Vision", 3, "UV patterns on feathers help them choose mates."),
190
+ ("Speech", 1, "Parrots mimic sound with a syrinx, not vocal cords."),
191
+ ("Speech", 2, "African greys can attach meaning to words."),
192
+ ("Speech", 3, "Wild flocks carry regional dialects."),
193
+ ]
194
+ OUTPUT = "facts.xlsx"
195
+
196
+ wb = Workbook()
197
+ spare = wb.active # removed at the end; every sheet is created below
198
+ written = {}
199
+ for row in ROWS:
200
+ title = row[0]
201
+ if title in written:
202
+ ws = wb[title]
203
+ else:
204
+ ws = wb.create_sheet(title)
205
+ ws.append(HEADERS)
206
+ for cell in ws[1]:
207
+ cell.font = Font(bold=True)
208
+ ws.column_dimensions[cell.column_letter].width = WIDTHS[cell.column - 1]
209
+ ws.freeze_panes = "A2"
210
+ written[title] = 0
211
+ ws.append(list(row[1:])) # row[1:] drops the sheet name — never append row
212
+ written[title] += len(row) - 1
213
+ ws.cell(row=ws.max_row, column=len(HEADERS)).alignment = Alignment(wrap_text=True)
214
+
215
+ wb.remove(spare)
216
+ assert written, "no rows were written"
217
+ for title in written:
218
+ rows_written = written[title] // len(HEADERS)
219
+ assert rows_written >= 3, f"sheet {title} holds {rows_written} row(s) — write at least 3"
220
+ wb.save(OUTPUT)
221
+ print(f"{len(wb.sheetnames)} sheets: " + ", ".join(f"{t}={written[t]}v" for t in written))
222
+ ```
223
+
224
+ **Append `list(row[1:])`, never `row` and never `[row]`.** `row` still carries the
225
+ sheet name, and `[row]` puts the whole tuple in one cell — that is what
226
+ `ValueError: Cannot convert (1, 'Parrots see …') to Excel` means. The slice is the
227
+ only unpacking this program does; do not add `for a, b in ROWS` loops of your own,
228
+ which raise `ValueError: too many values to unpack`.
229
+
230
+ A formula that references a sheet whose name contains a space must quote it:
231
+ `=SUM('Raw Data'!B1:B2)`.
232
+
233
+ **Do not invent a nested shape.** A list of `(title, [rows…])` pairs is the single
234
+ most common way this call fails: the inner list reaches a cell and openpyxl refuses
235
+ it. One flat list, sheet name first.
236
+
237
+ The count printed per sheet is **written values**, not rows. A row count proves
238
+ nothing: `ws.max_row` grows when a cell is merely styled, so a sheet that got
239
+ alignment but no values still reports four rows while holding nothing.
240
+
241
+ ## Embedding an Image
242
+
243
+ **The image must be staged in `inputs` in the same `exec` call.** The working
244
+ directory starts empty, so a file name that is not in `inputs` does not exist.
245
+ `XLImage("photo.png")` without an `inputs` entry whose `path` is `photo.png` raises
246
+ `FileNotFoundError`, and rerunning the same command fails the same way.
247
+
248
+ **`path` is a name you choose, not a name you derive.** It has nothing to do with
249
+ the attachment id: `photo.png` is a fine `path` for an attachment whose id is
250
+ `9f2c4ab1…`. Two mistakes to never make:
251
+
252
+ - Turning an id into a file name — `XLImage("9f2c4ab1….png")`, or opening the bare
253
+ id. An id goes in `attachmentId`, never in a path and never in the Python.
254
+ - Opening a name that merely appeared in the conversation. A `generate_image` result
255
+ carries a `fileName` — what the picture was called when it was made, not a file on
256
+ disk. Take the `attachmentId` from that result and ignore the rest; nothing is
257
+ readable until an `inputs` entry stages it under a `path` you chose.
258
+
259
+ **A `generate_image` result anywhere in the conversation IS the image the user
260
+ means.** "Add this image" points at the picture this chat already produced, in an
261
+ earlier turn as much as this one; the user's own message carries no attachment and
262
+ does not need to. Stage that id and build. Do not ask for an image the conversation
263
+ already has, and never generate a replacement to embed in its place.
264
+
265
+ An image the user **uploaded** has no id to copy. Stage it with `path` only and no
266
+ `attachmentId` key; the first id-less entry is the first image of their latest
267
+ message.
268
+
269
+ ```json
270
+ {
271
+ "language": "python",
272
+ "packages": ["openpyxl==3.1.5", "pillow"],
273
+ "inputs": [{ "attachmentId": "<id from the generate_image result>", "path": "photo.png" }],
274
+ "outputs": ["report.xlsx"],
275
+ "command": "..."
276
+ }
277
+ ```
278
+
279
+ The class lives in `openpyxl.drawing.image` and collides with PIL's `Image`, so
280
+ alias it. `add_image` anchors the picture's top-left corner at one cell:
281
+
282
+ ```python
283
+ from openpyxl.drawing.image import Image as XLImage
284
+
285
+ img = XLImage("photo.png") # the path from inputs, nothing else
286
+ img.width, img.height = 320, 320 # pixels
287
+ ws.add_image(img, "A1") # A1 is the top-left corner, not a range
288
+ ```
289
+
290
+ `add_image` is a **worksheet** method — `wb.add_image(...)` raises `AttributeError`.
291
+ The picture floats above the grid rather than filling a cell, so leave the rows it
292
+ covers empty.
293
+
294
+ **One `add_image` call, on one sheet.** "Add this image" means the workbook carries
295
+ the picture once — put it on the first sheet, or on a sheet of its own. Calling
296
+ `add_image` inside the loop that builds the sheets embeds a fresh copy per sheet.
297
+
298
+ **Check the call before sending it.** Every image file name in `command` must also
299
+ be the `path` of an `inputs` entry in that same call, and `packages` must include
300
+ `"pillow"`. The reverse holds too: if `inputs` stages an image, `command` must call
301
+ `add_image`. Never wrap `XLImage` or the import in a `try`/`except` that saves
302
+ anyway, and never draw a substitute with PIL — the image already exists.
303
+
304
+ Merged cells: after `ws.merge_cells("A1:C1")` only the top-left anchor is
305
+ writable — `ws["A1"] = "Title"` works, while writing `B1` or `C1` raises
306
+ `AttributeError: 'MergedCell' object attribute 'value' is read-only`. Write the
307
+ anchor, always.
308
+
309
+ ## Errors
310
+
311
+ - Never print the workbook's bytes or base64 — stdout is capped and the file
312
+ travels through `outputs`. A build call prints only a short summary line
313
+ (e.g. `2 sheets, 5 rows`).
314
+ - `SyntaxError: invalid syntax` on a one-line `for`/`if` means the source was
315
+ collapsed — restore multi-line newlines from the sample and rerun. Underscores
316
+ in names (`load_workbook`, not `loadworkbook`) must stay. Do not switch to
317
+ `python -c` or change the package pin.
318
+ - `ModuleNotFoundError: No module named 'openpyxl'` means `packages` was missing
319
+ or wrong — add `["openpyxl==3.1.5"]` and rerun. Never try to install it.
320
+ - `attachment … not found in this chat` means `inputs` listed an id that is not in
321
+ this chat (often a copied placeholder). For a new workbook, omit `inputs`
322
+ entirely and rerun.
323
+ - `ValueError: Cannot convert [...] to Excel` means a list or tuple reached a cell:
324
+ the code appended `row` or `[row]` instead of `list(row[1:])`, or invented a
325
+ nested `(title, [rows…])` shape. Flatten it — see Several Sheets.
326
+ - `ImportError: You must install Pillow to fetch image objects` means an image was
327
+ embedded without `"pillow"` in `packages` — openpyxl does not install it. Add it
328
+ and rerun.
329
+ - `ModuleNotFoundError: No module named 'PIL'` means the code imported PIL itself,
330
+ usually to draw a picture that already exists as an attachment. Stage the
331
+ attachment through `inputs` instead.
332
+ - `AttributeError: 'Workbook' object has no attribute 'add_image'` — `add_image` is
333
+ a worksheet method: `ws.add_image(img, "A1")`.
334
+ - `FileNotFoundError` on a 32-character hex name means an attachment id was opened
335
+ as a path. The id belongs in `attachmentId`; open the `path` you chose.
336
+ - A delivered workbook with no picture in it, when the user asked for one, means the
337
+ image was never staged or `add_image` was never called. Rebuild it in one call.
338
+ - `ImportError: cannot import name 'NumberFormat' from 'openpyxl.styles'` — there
339
+ is no such class. A number format is a plain string on the cell:
340
+ `cell.number_format = '#,##0.00'`. The only names to import from
341
+ `openpyxl.styles` are `Font`, `Alignment`, `PatternFill`, `Border`, `Side`.
342
+ - `TypeError: expected string or bytes-like object, got 'list'` means a worksheet
343
+ was indexed with a list — `ws[row]` where `row` holds the values just appended.
344
+ `ws[...]` takes a row number (`ws[1]`) or a range string (`ws["A1:C1"]`). To
345
+ style the row you just appended, index it by number: `ws[ws.max_row]`.
346
+ - `NameError: name 'openpyxl' is not defined` means the code called
347
+ `openpyxl.Workbook()` after `from openpyxl import Workbook` — call `Workbook()`.
348
+ - `NameError` on any other name means a variable was defined inside a `def` and
349
+ read outside it. Delete the function and inline its body at top level.
350
+ - `ValueError: month must be in 1..12, not 13` means the code did month
351
+ arithmetic. Type the periods out as literal values instead.
352
+ - `AssertionError: sheet <name> holds N row(s) — write at least 3` means a sheet got
353
+ fewer facts than the request implies. Add rows to that sheet and rerun; do not lower
354
+ the assert. **Row counts do not prove content**: `ws.max_row` counts a cell that was
355
+ merely styled, and is 1 on a truly empty sheet and never 0, so a header-only sheet
356
+ can report `4 rows` while holding nothing. Append values before styling anything.
357
+ - A bare `Sheet` among the sheet names is the untouched default sheet, left as a
358
+ blank first tab in front of the data. Either is a failed turn even at `exitCode 0`
359
+ — the user gets a blank or near-blank workbook. Rebuild it with literal rows.
360
+ - On an `AttributeError` from openpyxl the API name is wrong, and on a `TypeError`
361
+ about missing positional arguments a required argument was left out — fix either
362
+ against this file's examples. Do not retry the same call, and do not switch to a
363
+ shell.
364
+ - `'MergedCell' object attribute 'value' is read-only` means the write hit a merged
365
+ non-anchor cell — write the range's top-left cell instead.
366
+ - A formula cell that reads `None` or shows empty in a preview is not a bug —
367
+ this runtime computes nothing; the value appears when the user opens the file.
368
+ Do not rewrite the workbook to "fix" it.
369
+
370
+ ## Finish
371
+
372
+ When `exitCode` is `0` and `attachments` lists the `.xlsx`, stop tool use and
373
+ answer with one line: file name + the sheet/row summary from stdout. Exactly one
374
+ successful `exec` per request.