@qvac/skills 0.1.3 → 0.1.5

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/hash.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Autogenerated by scripts/build.mjs from skills/. Do not edit.
2
- export const SKILLS_HASH = 'c6c3741623f9af9c'
2
+ export const SKILLS_HASH = '8357531791b7cdb8'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/skills",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Skills for the QV.AC app — the SKILL.md tree plus a content-addressed bundle of it.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -1,15 +1,18 @@
1
1
  -- Create a Notes note. argv: name, htmlBody, [folder]
2
- -- Notes writes the `name` property as the body's first line, so creating with
3
- -- both name and body renders the title twice. Create from body, then rename.
2
+ -- A note's title is its first line. iCloud notes carry no separate `name` and
3
+ -- refuse `set name` (-10006) after the note already exists, which reported a
4
+ -- created note as failed. So the title goes into the body as its heading.
4
5
  on run argv
5
6
  set noteName to item 1 of argv
6
7
  set noteBody to item 2 of argv
8
+ set heading to "<h1>" & noteName & "</h1>"
9
+ if noteBody does not start with heading then set noteBody to heading & noteBody
7
10
  tell application "Notes"
8
11
  if (count of argv) > 2 then
9
12
  set newNote to make new note at folder (item 3 of argv) with properties {body:noteBody}
10
13
  else
11
14
  set newNote to make new note with properties {body:noteBody}
12
15
  end if
13
- set name of newNote to noteName
16
+ return id of newNote
14
17
  end tell
15
18
  end run
@@ -29,6 +29,15 @@ level so its double quotes need no escaping. Never embed a body inside
29
29
  `osascript -e '...'`: three nested quoting layers drop the closing `"`/`}` and
30
30
  produce `syntax error: Expected "}"` and no note.
31
31
 
32
+ On success the command prints the new note's id (`x-coredata://…`) and nothing
33
+ else. That id IS the confirmation: the note exists, so answer the user — never
34
+ run the command again to check or "retry". Only a non-zero exit with an
35
+ `execution error` means no note was created.
36
+
37
+ The note's title is its first line, so the script makes sure the body starts
38
+ with `<h1>name</h1>`, adding it when the body does not already begin with it.
39
+ Write the title once as the leading `<h1>` and the script changes nothing.
40
+
32
41
  Plain note in the default folder:
33
42
 
34
43
  ```bash
@@ -22,37 +22,25 @@ metadata:
22
22
 
23
23
  # Spotify
24
24
 
25
- Use `http_request` against `https://api.spotify.com/v1`. The Spotify credential is attached automatically to every `api.spotify.com` request — **never include an `auth` block**. Never invent track/album/artist URIs — search first and copy `uri` from the JSON response.
25
+ Use `http_request` against `https://api.spotify.com/v1`. The Spotify credential is attached automatically to every `api.spotify.com` request — **never include an `auth` block**.
26
26
 
27
- ```json
28
- {
29
- "url": "https://api.spotify.com/v1/search",
30
- "method": "GET",
31
- "query": { "q": "Radiohead Creep", "type": "track", "limit": 5 }
32
- }
33
- ```
34
-
35
- ## Hard rules
27
+ ## Playing a song takes two calls. Always two.
36
28
 
37
- - A bare song/artist/album/playlist name is enough — search immediately, take the top match, and tell the user what you picked. Do not ask "which one?" before searching.
38
- - Always search before playing by name. Play carries URIs only in the JSON `body` (`"uris": ["…"]`), never as query parameters.
39
- - A bare `PUT /me/player/play` with no body only resumes paused playback — it never plays a requested song. For an album/artist/playlist use `{ "context_uri": "<uri>" }` instead of `uris`.
40
- - Do not ask about tokens or setup up front. A **401** means Spotify isn't connected (tell the user to run `/connect spotify`). A **404** from `/me/player` means no active device (tell them to open Spotify).
29
+ "Play X" is not answered until **both** have run:
41
30
 
42
- ## Recipe: play a song by name
31
+ 1. `GET /v1/search` — find the track.
32
+ 2. `PUT /v1/me/player/play` — start it.
43
33
 
44
- 1. Search:
34
+ Search alone plays nothing. If you have searched and not yet called play, you are not finished: make the play call now.
45
35
 
46
36
  ```json
47
37
  {
48
38
  "url": "https://api.spotify.com/v1/search",
49
39
  "method": "GET",
50
- "query": { "q": "Radiohead Creep", "type": "track", "limit": 5 }
40
+ "query": { "q": "Radiohead Creep", "type": "track", "limit": 3, "market": "from_token" }
51
41
  }
52
42
  ```
53
43
 
54
- 2. Copy `tracks.items[0].uri` into the play body:
55
-
56
44
  ```json
57
45
  {
58
46
  "url": "https://api.spotify.com/v1/me/player/play",
@@ -61,6 +49,20 @@ Use `http_request` against `https://api.spotify.com/v1`. The Spotify credential
61
49
  }
62
50
  ```
63
51
 
52
+ Take `tracks.items[0].uri` from the search response and paste it into `uris`. A 204 means it started.
53
+
54
+ ## Hard rules
55
+
56
+ - **`q` carries every word the user named — the title and the artist.** "play Creep by Radiohead" searches `q=Radiohead Creep`, never `q=Creep`. Drop the artist and the top hit is a different band's song with the same title.
57
+ - **Before playing, check the item you picked.** Compare its `artists[0].name` with the artist the user named. If they do not match, take the first result that does. A title match under the wrong artist is the wrong song, and the user hears it immediately.
58
+ - **Never write a URI, a JSON block, or "I'll play it now" to the user in place of calling play.** Describing the call is not making it.
59
+ - **Every URI you send is one you copied from a search response in this turn.** Never type a `spotify:track:` id from memory or from an earlier turn. A well-formed id that is not real stops what was playing and starts nothing.
60
+ - **A track goes in `uris`. Only `uris`.** `context_uri` takes an album, artist or playlist URI — a track URI there plays nothing. URIs go in the JSON `body`, never in query parameters.
61
+ - **One call per intent.** A 204 means the call landed; do not send it again. Repeating a queue or play call burns the turn and changes nothing.
62
+ - **Name what actually played, read back from the item you used** — its `name` and `artists[0].name`. A 204 says the call was accepted, not which song it was, so never report a title you did not read out of the response.
63
+ - A bare song/artist/album/playlist name is enough — search immediately, take the top match, and tell the user what you picked. Do not ask "which one?" before searching.
64
+ - Do not ask about tokens or setup up front. A **401** means Spotify isn't connected (tell the user to run `/connect spotify`). A **404** from `/me/player` means no active device (tell them to open Spotify).
65
+
64
66
  ## Other operations
65
67
 
66
68
  All paths are under `https://api.spotify.com/v1`.
@@ -69,17 +71,20 @@ All paths are under `https://api.spotify.com/v1`.
69
71
  | --------------- | ----------------------------------------------------------------------- |
70
72
  | What's playing? | `GET /me/player/currently-playing` |
71
73
  | Pause | `PUT /me/player/pause` |
72
- | Resume | `PUT /me/player/play` (no body) |
74
+ | Resume | `PUT /me/player/play` (no body — resumes only, never starts a new song) |
73
75
  | Next track | `POST /me/player/next` |
74
76
  | Add to queue | `POST /me/player/queue` with `query`: `{ "uri": "spotify:track:<id>" }` |
77
+ | Play an album/artist/playlist | `PUT /me/player/play` with `body`: `{ "context_uri": "<uri>" }` |
75
78
  | My playlists | `GET /me/playlists` |
76
79
  | Top tracks | `GET /me/top/tracks` with `query`: `{ "time_range": "medium_term" }` |
77
80
  | Recently played | `GET /me/player/recently-played` |
78
81
  | List devices | `GET /me/player/devices` |
79
82
 
83
+ Queueing is the same two calls as playing: search for the track, then `POST /me/player/queue` with the `uri` you just read. Queueing does not start playback.
84
+
80
85
  ## Notes
81
86
 
82
- - Keep responses small — they truncate past 8KB, and raw JSON burns tokens on small models. Keep `limit` at 5. On playlist/track endpoints request only what you need with `fields` (e.g. `query`: `{ "fields": "items(track(name,artists(name),uri))" }`). Read just the top item unless the user asked for a list.
87
+ - Keep responses small — they truncate past 8KB, and raw JSON burns tokens on small models. Keep `limit` at 3 and **always pass `market`**: a search without it spends most of the budget on `available_markets`, and the results behind the first one are cut off before you can read them. `/search` has no `fields` param, so `market` is the only lever there. On playlist/track endpoints request only what you need with `fields` (e.g. `query`: `{ "fields": "items(track(name,artists(name),uri))" }`). Read just the top item unless the user asked for a list.
83
88
  - Present results as a short numbered list — track, artist, album, duration — and devices as `1. Name (active/idle)`. Never dump raw JSON to the user.
84
89
  - Playback commands return **204 No Content** on success (empty body). A **204** from currently-playing means nothing is playing.
85
90
  - **403** with `PREMIUM_REQUIRED` → user needs Spotify Premium. Other **403**s are usually app-scope/Development Mode limits — report and stop.
@@ -1,43 +1,59 @@
1
1
  ---
2
2
  name: weather
3
3
  description: Get current weather and short forecasts for cities via wttr.in.
4
- tools: [http_request]
4
+ tools: [weather_lookup]
5
5
  platform: [darwin, linux, win32, ios, android]
6
- allow_list: [https://wttr.in/]
7
- version: 3
8
- # Tuned on Qwen3.5-2B with an offline eval (33 prompts x mention/prose routes), QVAC-24701.
9
- # v2: few-shot table. 2 repeats: overall 51/132 -> 77/130, "Nassau, Bahamas" 1/4 -> 4/4, city+country 2/24 -> 18/24,
10
- # ambiguous city 0/20 -> 6/20; 4B control 41/66 -> 59/66. Rules-first layouts lost on suffix slips (?3, ?1T).
6
+ version: 4
7
+ # Tuned on Qwen3.5-2B with an offline eval (35 prompts, mention route), QVAC-24701.
8
+ # v2: few-shot table. 2 repeats: overall 51/132 -> 77/130, "Nassau, Bahamas" 1/4 -> 4/4, city+country 2/24 -> 18/24.
11
9
  # v3: + country row. 3 repeats: v2 110/198 -> v3 121/198 (city+country 25 -> 31/36, forecast 17 -> 23/30).
12
- # Aliases, a second ambiguity row, and dropping ?T were tried and did not help (73, 74, 54 of ~130).
10
+ # Aliases, a second ambiguity row and dropping ?T were tried and did not help (73, 74, 54 of ~130).
11
+ # v4: weather_lookup instead of http_request, after v3 answered a 200 for a place that does not exist.
12
+ # 5 repeats, all variants in one sweep so they share a baseline: v3 97/171 -> v4 120/169. city+country
13
+ # 87 -> 93%, unambiguous 57 -> 91%, unknown 57 -> 84%, forecast 68 -> 88%, casual 65 -> 80%, the ticket
14
+ # prompt 80 -> 100%. Context is a wash (2335 -> 2378) though the body is 387 bytes smaller: the tool owns
15
+ # the URL, so three rules about spelling one went away.
16
+ # Not fixed. "Ask which Nassau" is 3/25 on v3 and 0/25 here, and asking when no place is named is 1/10 for
17
+ # both: a one-argument call is easy, so the model calls rather than asks. Rules that tell the model not to
18
+ # act have never cleared ~17% on a 2B in four versions, and a variant that made the tool refuse an
19
+ # ambiguous name measured 13% against 17%, so it was dropped rather than shipped.
13
20
  ---
14
21
 
15
22
  # Weather
16
23
 
17
- One `http_request` to wttr.in, then answer from the body. Always call it exactly like this, with `"method": "GET"`:
24
+ One `weather_lookup` call, then answer from the result. Always call it exactly like this:
18
25
 
19
26
  ```json
20
- { "url": "https://wttr.in/Nassau,+Bahamas?format=3", "method": "GET" }
27
+ { "location": "Nassau, Bahamas" }
21
28
  ```
22
29
 
23
- Match the user's request to a row for the URL:
30
+ Match the user's request to a row for the call:
24
31
 
25
32
  | User asks | Call |
26
33
  | --- | --- |
27
- | "weather in Nassau, Bahamas" | `https://wttr.in/Nassau,+Bahamas?format=3` |
28
- | "weather in London" / "London today" | `https://wttr.in/London?format=3` |
29
- | "Berlin tomorrow" / "this weekend" | `https://wttr.in/Berlin?2T` |
30
- | "Rome for the next 3 days" | `https://wttr.in/Rome?T` |
31
- | "how hot is it in Georgia, the country" | `https://wttr.in/Tbilisi,+Georgia?format=3` (a country → its capital) |
32
- | "weather in Nassau" (Nassau exists in the Bahamas and in New York) | no call — ask: "Which Nassau do you mean, the Bahamas or New York?" |
33
- | "what's the weather?" (no place) | no call — ask which city |
34
+ | "what's the weather?" — no place named | **no call.** Ask which city |
35
+ | "weather in Nassau, Bahamas" | `{ "location": "Nassau, Bahamas" }` |
36
+ | "weather in London" / "London today" | `{ "location": "London" }` |
37
+ | "Berlin tomorrow" / "this weekend" | `{ "location": "Berlin" }` |
38
+ | "Rome for the next 3 days" | `{ "location": "Rome" }` |
39
+ | "how hot is it in Georgia, the country" | `{ "location": "Tbilisi, Georgia" }` (a country → its capital) |
40
+ | "weather in Nassau" | `{ "location": "Nassau" }` — the result names both, ask which |
41
+
42
+ The result is the place on the first line, then the weather now, then one line per day:
43
+
44
+ ```
45
+ Nassau, New Providence, Bahamas
46
+ Now: 29°C / 84°F, Patchy rain nearby, feels like 33°C, humidity 71%, wind 21 km/h
47
+ 2026-09-16 (today): 29-29°C / 84-85°F, Partly Cloudy
48
+ 2026-09-17 (tomorrow): 28-29°C / 83-85°F, Moderate or heavy rain shower
49
+ ```
34
50
 
35
51
  ## Rules
36
52
 
37
- 1. **The location is exactly what the user wrote, spaces as `+`.** Keep a country or state they gave (`Nassau,+Bahamas`, not `Nassau`). Never add one they did not give. A country → its capital (`Tbilisi`).
38
- 2. **Ambiguous city names — Nassau, Springfield, Portland, Cambridge, Georgia, San Jose, Birmingham — with no country or state → do not call, ask which one.**
39
- 3. **The URL ends in `?format=3`, `?2T` or `?T`. Nothing else.** `?format=3` is the default for now/today/current. Write it in full: `London?3`, `London?format=3&format=3` and a bare `London` are all wrong.
40
- 4. **Status not 200, or body `location not found` → say the place could not be found and ask for a more specific name. Do not retry other cities. Never state a temperature you did not receive.**
41
- 5. wttr.in stops at 3 days: for "next week" say so and offer `?T`.
53
+ 1. **There is no default place.** If the user named none, do not call: ask which city. Never use a place from this file.
54
+ 2. **The location is exactly what the user wrote.** Keep a country or state they gave (`Nassau, Bahamas`, not `Nassau`). Never add one they did not give. A country → its capital (`Tbilisi`).
55
+ 3. **Name the place from the first line of the result, not the words the user used.** If they ask for Rome and the first line says `Lome, Maritime, Togo`, say it is Lome in Togo.
56
+ 4. **If the result is not a weather report, say what it says, in those words.** `No such place: "Xyzzyville, Atlantis"` → tell the user that place does not exist and ask for a real one. Do not look it up again under another name, and never state a temperature you did not receive.
57
+ 5. Forecasts stop at 3 days: for "next week" say so and give the 3 days you have.
42
58
 
43
- A `200` body like `Nassau, Bahamas: 🌦️ +30°C` is the answer: give that temperature and condition in one plain sentence.
59
+ Answer in one plain sentence with the temperature and the condition.
@@ -19,43 +19,35 @@ metadata:
19
19
 
20
20
  # Word
21
21
 
22
- Build, edit, or read `.docx` documents by running python-docx through the
23
- `exec` tool with `language: "python"`. Declare a produced document in
24
- `outputs` and it comes back as a chat attachment the user can save. To answer
25
- _from_ a document instead of building one, run a read call — no `outputs` —
26
- and reply in the chat.
27
-
28
- ## Load the Recipe File First
29
-
30
- This file contains no Python. The working recipes live in four reference
31
- files — load the one for the job with the `skill` tool BEFORE writing any
32
- Python, then copy its recipe and change the content:
22
+ Build, change, or read `.docx` documents. This file holds no Python and no
23
+ recipe: it only says which reference file to load. Load exactly one with the
24
+ `skill` tool, then do what that file says.
25
+
26
+ ## Which File to Load
27
+
28
+ Pick the row by **what the user wants done**, then make that exact `skill`
29
+ call. "Edit", "update", "modify", "change", "replace", "rewrite" and "fix" all
30
+ mean the same thing here — the verb never picks the row, the change does.
31
+
32
+ | The user wants | The `skill` call |
33
+ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
34
+ | A new document — "write a report", "make a doc with 30 fun facts about cats", "draft a letter" | `{"name": "word", "file": "references/create.md"}` |
35
+ | Some paragraphs of an existing document changed — "replace the first 10 facts with dog facts", "change fact 3", "reword paragraph 7", "swap these bullets for those" | `{"name": "word", "file": "references/paragraphs.md"}` |
36
+ | Anything else done to an existing document — add a section, remove or rewrite a whole section, make the text bigger, put an image in it | `{"name": "word", "file": "references/rework.md"}` |
37
+ | An answer in the chat from an attached document — "summarize this", "what does it say about X" | `{"name": "word", "file": "references/read.md"}` |
38
+
39
+ - A document that already exists in this chat is never rebuilt with
40
+ `create.md` — that throws away everything the user has. Its `attachmentId`
41
+ is in the `exec` result that produced it or on the `[Attached file …]` line.
42
+ - A summary delivered as a file is `read.md` first, then `create.md`.
43
+ - `paragraphs.md` runs two scripts bundled with this skill and contains no
44
+ Python. `rework.md` and `create.md` carry the python-docx recipes to copy.
33
45
 
34
46
  Each load is a real `skill` tool call — printing the call as JSON or text in
35
- your reply loads nothing.
36
-
37
- - **Creating a new document** (no existing `.docx` involved; may embed
38
- images): call the `skill` tool with `name: "word"` and
39
- `file: "references/create.md"`.
40
- - **Changing some of the facts, points, bullets, items, or paragraphs** of an
41
- existing `.docx` — "replace the first 10 facts", "change fact 3", "swap the
42
- bullets for these", "reword paragraph 7": call the `skill` tool with
43
- `name: "word"` and `file: "references/replace.md"`. This is the file even
44
- when the user says edit, replace, change, update, or rewrite; it runs two
45
- bundled scripts and no Python is written.
46
- - **Any other edit of an existing document** (extend it, trim it, rework a
47
- whole section, resize the text, embed an image into it): call the `skill`
48
- tool with `name: "word"` and `file: "references/edit.md"`.
49
- - **Reading a document to answer in chat** (a summary, a question answered,
50
- content pulled out — no file delivered): call the `skill` tool with
51
- `name: "word"` and `file: "references/read.md"`.
52
- - **A summary delivered as a file** is a read followed by a build: load both
53
- `references/read.md` and `references/create.md`.
54
-
55
- Never write the Python from memory. The recipes carry rules (exact version
56
- pins, attachment staging, run-level formatting, in-place replacement, the only
57
- working removal idiom) that fail in non-obvious ways when improvised; loading
58
- the file is one cheap read-only call.
47
+ your reply loads nothing. Never write the Python from memory: the recipes carry
48
+ rules (exact version pins, attachment staging, the only working removal idiom)
49
+ that fail in non-obvious ways when improvised, and loading the file is one
50
+ cheap read-only call.
59
51
 
60
52
  ## When to Use
61
53
 
@@ -106,13 +98,15 @@ no-`outputs` read that precedes a build delivers nothing and is not one of
106
98
  them, but it belongs before the build, never after it. Reply with a single
107
99
  line: file name + the count line from stdout. If the result has
108
100
  `missingOutputs` instead, the file was never written: read stderr first — an
109
- `AssertionError` there means a guard stopped the save on purpose (see the edit
110
- recipe); only when stderr is clean check the `save()` name matches the
101
+ `AssertionError` there means a guard stopped the save on purpose (see the
102
+ rework recipe); only when stderr is clean check the `save()` name matches the
111
103
  declared output and rerun once.
112
104
 
113
105
  **Failures are fixed in the code, not around it.** An error in your code is
114
106
  never a fault in python-docx or in the runtime; fix the Python against the
115
- loaded reference file's recipes and Errors and call `exec` again. If two
107
+ loaded reference file's recipes and Errors and call `exec` again. A bundled
108
+ script that stops with a message is fixed by correcting its arguments and
109
+ rerunning the same script — never by writing Python in its place. If two
116
110
  consecutive calls fail with the same error, re-read the traceback
117
111
  line-by-line before a third — retrying the identical `command`, or a version
118
112
  with only cosmetic changes, is a loop, not a fix. Do not switch package pins
@@ -130,12 +124,12 @@ and a file you write but do not declare in `outputs` is discarded. The `exec`
130
124
  result is the only account of what happened — there is no filesystem to check
131
125
  and no shell to check it with.
132
126
 
133
- **Never overwrite a staged input.** Edits always save a new output name,
134
- derived from the document edited — `report.docx` becomes `report_revised.docx`,
127
+ **Never overwrite a staged input.** Changes always save a new output name,
128
+ derived from the document changed — `report.docx` becomes `report_revised.docx`,
135
129
  never a fresh name taken from the new content.
136
130
 
137
- **An edit changes the document in place.** `add_paragraph` and `add_heading`
131
+ **A change happens inside the document.** `add_paragraph` and `add_heading`
138
132
  append at the end and nowhere else, so replacing content that is already there
139
133
  means rewriting those paragraphs, not adding new ones. Delivering the original
140
- with the new version appended is a failed turn — the edit recipe carries the
141
- guards that catch it.
134
+ with the new version appended, or a fresh document holding only the new
135
+ content, is a failed turn.
@@ -5,11 +5,12 @@ A new document needs **no** `inputs` — do not invent attachment ids — unless
5
5
  it embeds an image (see Embedding Images). **Exactly one** `exec` call per
6
6
  user request when that call succeeds.
7
7
 
8
- **A document that already exists in this chat is never rebuilt here.** "Add a
9
- section", "reword this", "extend the doc" — any request that starts from an
10
- existing `.docx` is an EDIT: load `references/edit.md` and stage the document
11
- by its `attachmentId`. Building a fresh document for an edit request throws
12
- away everything the user already has.
8
+ **A document that already exists in this chat is never rebuilt here.** "Replace
9
+ the first 10 facts", "reword this", "add a section" — any request that starts
10
+ from an existing `.docx` is a change to that document: some of its paragraphs
11
+ is `references/paragraphs.md`, anything else is `references/rework.md`, and
12
+ either one stages the document by its `attachmentId`. Building a fresh
13
+ document for such a request throws away everything the user already has.
13
14
 
14
15
  ## The exec call
15
16
 
@@ -81,9 +82,40 @@ through other tools — that is a dead end. Say the link cannot be downloaded
81
82
  and ask the user to attach the image itself, or offer `generate_image` for a
82
83
  similar visual. Then build the document with the staged attachment as above.
83
84
 
85
+ ## Which Shape
86
+
87
+ | The user asks for | Shape |
88
+ | ---------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
89
+ | "30 fun facts about cats", "10 tips for …", "a list of …", any number of items or points | **List** — a title, then exactly N `List Bullet` paragraphs and nothing else: no intro sentence, no section headings, no numbers typed into the text |
90
+ | a report, memo, letter, plan — anything with sections | **Report** — the recipe under The Recipe below |
91
+
92
+ ### The list shape
93
+
94
+ ```python
95
+ from docx import Document
96
+
97
+ doc = Document()
98
+ doc.add_heading("30 Fun Facts About Cats", level=0)
99
+ facts = [
100
+ "Cats sleep for about 70 percent of their lives.",
101
+ "A group of cats is called a clowder.",
102
+ "A cat's nose print is unique, like a fingerprint.",
103
+ ] # one plain string per item — write all N here
104
+ for fact in facts:
105
+ doc.add_paragraph(fact, style="List Bullet")
106
+ doc.save("cat_facts.docx") # must match the declared output exactly
107
+ print(f"{len(facts)} items, {len(doc.paragraphs)} paragraphs, {len(doc.tables)} table(s)")
108
+ ```
109
+
110
+ One string per item, as many as the user asked for. No headings between
111
+ groups of items and no introductory sentence: each of those is a paragraph the
112
+ user did not ask for, and a later "change the first 10 items" then lands on the
113
+ wrong lines. The count line it prints is the reply — a delivered list is done,
114
+ whatever the count says; never rebuild it to fix the number.
115
+
84
116
  ## The Recipe
85
117
 
86
- Start from this. It is a complete, working document — a title, headings,
118
+ Start from this for a report. It is a complete, working document — a title, headings,
87
119
  paragraphs with bold and italic runs, a bulleted list, and a table — saved
88
120
  under the declared output name. Copy it and change the content; do not
89
121
  assemble a document from memory.
@@ -255,7 +287,7 @@ A **new** `Document()` ships these styles — safe to use without checking:
255
287
  `Title`, `Heading 1` … `Heading 9`, `Normal`, `List Bullet` (+ ` 2`, ` 3`),
256
288
  `List Number` (+ ` 2`, ` 3`), `Intense Quote`, and the table style `Table Grid`.
257
289
  Do not invent other names for a new document. (An uploaded document carries
258
- only its own styles — when editing one, load `references/edit.md` for the
290
+ only its own styles — when editing one, load `references/rework.md` for the
259
291
  guard.)
260
292
 
261
293
  ## Headings and lists
@@ -1,11 +1,31 @@
1
- # Replacing Paragraphs by Position (bundled scripts)
1
+ # Changing Some Paragraphs of a Document (bundled scripts)
2
2
 
3
- "Replace the first 10 facts", "reword point 3", "swap these bullets for those",
4
- "change paragraph 7": two `exec` calls, both running a script bundled with this
5
- skill. **Write no Python.** Never put source in `command` for this job — pass
6
- `skill`, `script`, and `scriptArgs` exactly as shown, with `inputs` staging the
7
- document by its `attachmentId` (from the earlier `exec` result or the
8
- `[Attached file …]` line — copy it verbatim, never invent one).
3
+ "Replace the first 10 facts", "change fact 3", "swap these bullets for those",
4
+ "reword paragraph 7": two `exec` calls, both running a script bundled with this
5
+ skill. **Write no Python.** There is no `command` in this job — a call with
6
+ `command` is the wrong call. Pass `skill`, `script`, and `scriptArgs` exactly as
7
+ shown, with `inputs` staging the document by its `attachmentId` (from the
8
+ earlier `exec` result or the `[Attached file …]` line — copy it verbatim, never
9
+ invent one).
10
+
11
+ | Step | The `exec` call |
12
+ | --------------------------------------- | ---------------------------------------------------------------- |
13
+ | 1. see the paragraphs and their indexes | `scripts/list_paragraphs.py`, `inputs` staged, no `outputs` |
14
+ | 2. replace exactly the chosen indexes | `scripts/replace_paragraphs.py`, `inputs` staged, one `outputs` |
15
+
16
+ ## Step 0 — find the document's `attachmentId`
17
+
18
+ The id is in the chat already, never invented: a document built earlier in
19
+ this chat has it in the `attachments` of the `exec` result that produced it —
20
+ `{"attachmentId":"922bd4e17517b90593be1c5ae4f12fbd","fileName":"cat_facts.docx"}`
21
+ — and a document the user uploaded has it on the `[Attached file …]` line of
22
+ their message. Copy that exact id into `inputs`. An `inputs` entry with a
23
+ `path` and no `attachmentId` is an _image_ upload and is refused for a
24
+ document:
25
+
26
+ ```json
27
+ { "inputs": [{ "path": "existing.docx" }] }
28
+ ```
9
29
 
10
30
  ## Step 1 — list the paragraphs (no `outputs`)
11
31
 
@@ -20,6 +40,10 @@ document by its `attachmentId` (from the earlier `exec` result or the
20
40
  }
21
41
  ```
22
42
 
43
+ Every key above is required — `inputs` with the document's real
44
+ `attachmentId`, `skill`, `script`, `scriptArgs`. A call missing `skill` or
45
+ `inputs` is refused.
46
+
23
47
  It prints one line per paragraph — `index`, style, text — then a count line.
24
48
  Pick the indexes to replace from that list:
25
49
 
@@ -27,7 +51,7 @@ Pick the indexes to replace from that list:
27
51
  points, or bullets. `Title`, `Heading N`, and an intro sentence are never
28
52
  counted as one.
29
53
  - "The first 10 facts" = the first 10 body-paragraph indexes after the heading
30
- that introduces them — not indexes 0–9.
54
+ or sentence that introduces them — not indexes 0–9.
31
55
  - Fewer facts in the document than asked for: replace the ones that exist and
32
56
  say so in the reply.
33
57
 
@@ -57,6 +81,12 @@ start + 9.
57
81
  }
58
82
  ```
59
83
 
84
+ Wrong, for this job — a `command` instead of a `script`:
85
+
86
+ ```json
87
+ { "command": "from docx import Document\ndoc = Document(\"existing.docx\")\nfor i in range(1, 11): ..." }
88
+ ```
89
+
60
90
  Each new text is one complete plain sentence, no markdown, each different. The
61
91
  script keeps each paragraph's paragraph style, refuses a heading index,
62
92
  refuses text that already reads the same, and prints
@@ -74,6 +104,10 @@ rerun the **same script** — never switch to writing Python.
74
104
 
75
105
  - `usage: replace_paragraphs.py …` — the pairs are incomplete: after the two
76
106
  file names, arguments alternate `index`, `text`.
107
+ - `scriptArgs name "existing_revised.docx" but the working directory starts
108
+ empty` — the call has no `outputs`; add `"outputs": ["existing_revised.docx"]`
109
+ (the same name as in `scriptArgs`) and rerun the same script.
110
+ - `script runs need the owning skill name in skill` — add `"skill": "word"`.
77
111
  - `index N is the heading '…'` — that paragraph is a heading, not a fact. Pick
78
112
  body indexes from the Step 1 list.
79
113
  - `index N is outside the document's M paragraphs` — re-read the Step 1 list;
@@ -82,5 +116,10 @@ rerun the **same script** — never switch to writing Python.
82
116
  a different sentence.
83
117
  - `output … must be a new name` — the output name equals the input's; use
84
118
  `existing_revised.docx`.
85
- - `PackageNotFoundError` / `attachment … not found` — `inputs` is missing or
86
- carries an invented id; stage the document by its real `attachmentId`.
119
+ - `an id-less input stages an uploaded image` — the `inputs` entry has no
120
+ `attachmentId`; add the document's id from Step 0 and rerun the same script.
121
+ - `usage: list_paragraphs.py <input.docx>` — `scriptArgs` was left out; pass
122
+ the staged path, `["existing.docx"]`.
123
+ - `PackageNotFoundError` / `attachment … not found` / `does not exist in the
124
+ working directory` — `inputs` is missing or carries an invented id; stage the
125
+ document by its real `attachmentId`.
@@ -1,16 +1,79 @@
1
- # Editing an Existing Word Document (python-docx)
2
-
3
- **Stop here if the request changes some of the facts, points, bullets, items,
4
- or paragraphs** — "replace the first 10 facts", "change fact 3", "swap the
5
- bullets", "reword paragraph 7". That job is `references/replace.md`: call the
6
- `skill` tool with `name: "word"` and `file: "references/replace.md"` now, and
7
- do not use anything in this file for it. No Python is written for that job.
1
+ # Reworking an Existing Word Document (python-docx)
8
2
 
9
3
  Change, replace, extend, trim, or rework a `.docx` that is already in this
10
4
  chat by running Python through the `exec` tool: stage it as an input, modify
11
5
  paragraphs and tables, and save a **new** output such as
12
6
  `existing_revised.docx`. Never overwrite the staged input.
13
7
 
8
+ ## Replacing Some Facts, Points, Bullets or Paragraphs: Two Script Calls
9
+
10
+ "Replace the first 10 facts", "change fact 3", "swap these bullets for those",
11
+ "reword paragraph 7" — any request that changes some of the paragraphs and
12
+ keeps the rest — is two `exec` calls that run scripts bundled with this skill.
13
+ **Write no Python for it.** Nothing else in this file applies to that job: no
14
+ `command`, no `Document(...)`, no fingerprint, no loop. `references/paragraphs.md`
15
+ is this same recipe with its error table.
16
+
17
+ The document's `attachmentId` is already in the chat — in the `attachments` of
18
+ the `exec` result that produced it, or on the user's `[Attached file …]` line.
19
+ Copy it into `inputs`; an entry with only a `path` stages an image, not a
20
+ document.
21
+
22
+ Step 1 — list the paragraphs (no `outputs`):
23
+
24
+ ```json
25
+ {
26
+ "language": "python",
27
+ "packages": ["python-docx==1.2.0"],
28
+ "inputs": [{ "attachmentId": "<real id>", "path": "existing.docx" }],
29
+ "skill": "word",
30
+ "script": "scripts/list_paragraphs.py",
31
+ "scriptArgs": ["existing.docx"]
32
+ }
33
+ ```
34
+
35
+ It prints one line per paragraph — `index`, style, text — then a count line.
36
+ Only body paragraphs (`Normal`, `List Bullet`, `List Number`) are facts, points,
37
+ or bullets; `Title`, `Heading N`, and an intro sentence are never counted as
38
+ one. "The first 10 facts" = the first 10 body-paragraph indexes after the
39
+ heading or sentence that introduces them — not indexes 0–9.
40
+
41
+ Step 2 — replace exactly those paragraphs (one `outputs` entry). `scriptArgs`
42
+ is the input name, the output name, then one `index, new text` pair per
43
+ replaced paragraph — "the first 10 facts" is 10 pairs, each a different
44
+ complete sentence:
45
+
46
+ ```json
47
+ {
48
+ "language": "python",
49
+ "packages": ["python-docx==1.2.0"],
50
+ "inputs": [{ "attachmentId": "<real id>", "path": "existing.docx" }],
51
+ "outputs": ["existing_revised.docx"],
52
+ "skill": "word",
53
+ "script": "scripts/replace_paragraphs.py",
54
+ "scriptArgs": [
55
+ "existing.docx", "existing_revised.docx",
56
+ "3", "Dogs have about 1,700 taste buds.",
57
+ "4", "A dog's nose print is unique, like a fingerprint."
58
+ ]
59
+ }
60
+ ```
61
+
62
+ Wrong, for this job — a `command` instead of a `script`:
63
+
64
+ ```json
65
+ { "command": "from docx import Document\ndoc = Document(\"existing.docx\")\nfor i in range(1, 11): ..." }
66
+ ```
67
+
68
+ `exitCode 0` plus an attachment = done: reply with the file name and the
69
+ printed `K of N paragraphs replaced`, and do not call `exec` again. A script
70
+ error names the fix (a heading index, an index out of range, unchanged text,
71
+ missing pairs, a missing `outputs` for the revised name); correct the
72
+ arguments and rerun the **same script**.
73
+
74
+ Everything below is for the other edits: extending a document, trimming it,
75
+ rewriting a whole section, resizing its text, embedding an image.
76
+
14
77
  ## Staging the Document
15
78
 
16
79
  Stage the document as an input **by its `attachmentId`** and open it with
@@ -163,9 +226,9 @@ formatting-loss rule below.
163
226
  The `add_heading`/`add_paragraph` pair above appends an **Appendix** because
164
227
  that is what the sample edit asks for. Copy that shape only when the user
165
228
  genuinely wants new content at the end. Substituting content that is already
166
- in the document — "change the first five points", "rewrite section 2" — is a
167
- different job with its own recipe and its own guards: see Replacing Content In
168
- Place.
229
+ in the document is a different job with its own guards: some of the points —
230
+ "change the first five points" — is the two script calls at the top of this
231
+ file; a whole section — "rewrite section 2" — is Rewriting Whole Sections.
169
232
 
170
233
  When an edit adds substantial new content — new sections, formatted runs,
171
234
  bulleted lists, whole tables — the writing rules apply unchanged: load
@@ -193,11 +256,11 @@ bullet = "List Bullet" if "List Bullet" in names else None
193
256
  doc.add_paragraph("point one", style=bullet) # style=None → Normal
194
257
  ```
195
258
 
196
- ## Replacing Content In Place
259
+ ## Rewriting Whole Sections
197
260
 
198
- This recipe is for whole *sections* (a heading plus its body). Changing some
199
- of the facts, points, bullets, or paragraphs is `references/replace.md` —
200
- never hand-write a loop for that.
261
+ This recipe is for whole _sections_ (a heading plus its body). Changing some
262
+ of the facts, points, bullets, or paragraphs is the two script calls at the
263
+ top of this file — never hand-write a loop for that.
201
264
 
202
265
  `add_paragraph`, `add_heading`, and `add_picture` **always append at the end of
203
266
  the document.** None of them takes a position. "Change the first five points",