@floh-solutions/pharos-cli 0.4.0 → 0.13.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 (67) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +418 -7
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/attach.d.ts +30 -0
  5. package/dist/commands/attach.d.ts.map +1 -0
  6. package/dist/commands/attach.js +55 -0
  7. package/dist/commands/attach.js.map +1 -0
  8. package/dist/commands/create.d.ts +35 -0
  9. package/dist/commands/create.d.ts.map +1 -0
  10. package/dist/commands/create.js +82 -0
  11. package/dist/commands/create.js.map +1 -0
  12. package/dist/commands/detach.d.ts +34 -0
  13. package/dist/commands/detach.d.ts.map +1 -0
  14. package/dist/commands/detach.js +117 -0
  15. package/dist/commands/detach.js.map +1 -0
  16. package/dist/commands/discover.d.ts +66 -0
  17. package/dist/commands/discover.d.ts.map +1 -0
  18. package/dist/commands/discover.js +128 -0
  19. package/dist/commands/discover.js.map +1 -0
  20. package/dist/commands/hooks.d.ts +24 -0
  21. package/dist/commands/hooks.d.ts.map +1 -0
  22. package/dist/commands/hooks.js +184 -0
  23. package/dist/commands/hooks.js.map +1 -0
  24. package/dist/commands/image.d.ts +44 -0
  25. package/dist/commands/image.d.ts.map +1 -0
  26. package/dist/commands/image.js +114 -0
  27. package/dist/commands/image.js.map +1 -0
  28. package/dist/commands/import.d.ts +51 -0
  29. package/dist/commands/import.d.ts.map +1 -0
  30. package/dist/commands/import.js +190 -0
  31. package/dist/commands/import.js.map +1 -0
  32. package/dist/commands/link.d.ts +40 -0
  33. package/dist/commands/link.d.ts.map +1 -0
  34. package/dist/commands/link.js +150 -0
  35. package/dist/commands/link.js.map +1 -0
  36. package/dist/commands/people.d.ts +39 -0
  37. package/dist/commands/people.d.ts.map +1 -0
  38. package/dist/commands/people.js +64 -0
  39. package/dist/commands/people.js.map +1 -0
  40. package/dist/commands/query.d.ts +43 -0
  41. package/dist/commands/query.d.ts.map +1 -0
  42. package/dist/commands/query.js +202 -0
  43. package/dist/commands/query.js.map +1 -0
  44. package/dist/commands/recycle.d.ts +10 -0
  45. package/dist/commands/recycle.d.ts.map +1 -0
  46. package/dist/commands/recycle.js +118 -0
  47. package/dist/commands/recycle.js.map +1 -0
  48. package/dist/commands/task.d.ts +13 -0
  49. package/dist/commands/task.d.ts.map +1 -1
  50. package/dist/commands/task.js +106 -4
  51. package/dist/commands/task.js.map +1 -1
  52. package/dist/commands/update.d.ts +52 -0
  53. package/dist/commands/update.d.ts.map +1 -0
  54. package/dist/commands/update.js +181 -0
  55. package/dist/commands/update.js.map +1 -0
  56. package/dist/commands/wiki.d.ts.map +1 -1
  57. package/dist/commands/wiki.js +133 -2
  58. package/dist/commands/wiki.js.map +1 -1
  59. package/dist/output.d.ts.map +1 -1
  60. package/dist/output.js +32 -2
  61. package/dist/output.js.map +1 -1
  62. package/dist/wiql-build.d.ts +92 -0
  63. package/dist/wiql-build.d.ts.map +1 -0
  64. package/dist/wiql-build.js +147 -0
  65. package/dist/wiql-build.js.map +1 -0
  66. package/package.json +2 -2
  67. package/skill/SKILL.md +388 -62
package/skill/SKILL.md CHANGED
@@ -1,62 +1,394 @@
1
1
  ---
2
2
  name: pharos
3
- description: "Use whenever Azure DevOps work is involved reading or updating a work item, task, bug, story or epic; anything with a board, backlog, sprint or iteration; reading or writing a project wiki page; commenting on a task or a wiki page; linking a plan to an epic; or when the user names a work item by number (\"pick up 4821\", \"what's on 210\"). Also use when deciding between the Azure DevOps MCP server and the `pharos` CLI, or when an Azure DevOps call fails and you need to know whether to retry it."
3
+ description: "Use for AZURE DEVOPS work specifically — when the user says Azure DevOps, ADO or dev.azure.com, or names an ADO work item by number (\"pick up 4821\", \"what's on 210\"). Covers reading or updating an ADO work item, ticket, bug, story or epic; the ADO board, backlog, sprint or iteration; what is assigned to you in Azure DevOps; reading or writing an ADO project wiki page and its comments; linking a plan to an epic; attaching a file to a work item or taking one off; putting an inline image into a description, a comment, a wiki page or a wiki comment; @mentioning somebody so they are actually notified; importing Markdown, Word or PDF files as wiki pages; and whether a failed Azure DevOps call is worth retrying. NOT for other trackers the Argus board, GitHub issues, Jira, Linear — where \"task\", \"todo\" and \"board\" mean something else entirely."
4
4
  license: Proprietary
5
5
  ---
6
6
 
7
7
  # Working Azure DevOps with `pharos`
8
8
 
9
- `pharos` is a CLI that gives you the full Azure DevOps surface from a shell,
10
- authenticated by an environment variable rather than a browser login. Run
11
- `pharos --help` for the complete verb list; this skill is the part that is not
12
- in the help text.
9
+ `pharos` is a CLI that gives you Azure DevOps from a shell, authenticated by an
10
+ environment variable rather than a browser login.
13
11
 
14
- ## Start every task with one command
12
+ **Everything it can do is listed below. Do not run `pharos --help` to find
13
+ out** — that costs several calls and this section is the same information.
14
+
15
+ ```
16
+ whoami who ADO_PAT belongs to. @Me resolves to this
17
+ types what --state and --type will ACCEPT, per type
18
+ query WHICH work items — assigned to you, in a sprint,
19
+ of a type, still open. Hydrated items, not ids.
20
+ task <id> one work item, whole: fields, comments,
21
+ attachments, relations WITH titles, and the
22
+ content + discussion of every linked wiki page
23
+ --children an EPIC and everything under it, in
24
+ ONE call. --depth <n> for deeper (max 5)
25
+ --compact flatten identities, drop board keys
26
+ create <type> --title "…" one work item. --parent goes in the SAME patch
27
+ update <id> change a field: --state --priority --assignee
28
+ --title, or --field Name=value for anything else
29
+ link <id> --parent <id> relate two items. Also --child --related
30
+ unlink <id> --parent <id> --predecessor --successor --duplicate
31
+ attach <id> <file> put a FILE on a work item (Attachments list)
32
+ detach <id> <url-or-guid> take one off. --yes
33
+ download <id-or-url> read one back. --out <path> or it writes nothing
34
+ image <file> upload a PICTURE for use inside text. Prints the
35
+ markdown to paste. NOT the same as attach
36
+ people [query] who can be @mentioned, with the @<guid> form
37
+ delete <id> --yes → Recycle Bin (no permanent delete, on purpose)
38
+ restore <id> bring one back
39
+ deleted what is in the Recycle Bin, with names
40
+ wiki list | tree | read <path> | write <path> | delete <path>
41
+ wiki move <path> <new path> move a page; sub-pages come with it
42
+ wiki rename <path> <new name> the same call, leaf only. BOTH need --yes
43
+ wiki duplicate <path> [to] a verb Azure DevOps lacks. "<path> - Copy N"
44
+ wiki image <file> a picture for a PAGE or page comment. Different
45
+ endpoint from `image`, and the name is unique-d
46
+ wiki import <file...> .md .txt .docx .pdf .rtf .html -> pages
47
+ --under <path> --as <name>
48
+ comment list | add | edit | delete <target> is a work item id OR a wiki path
49
+ comment react | unreact | reactors like dislike heart hooray smile confused
50
+ hooks list | check | create | repoint | delete service hooks for realtime
51
+ plan <file> an implementation plan → a work item tree
52
+ setup org, project, token → keychain + shell profile
53
+ ```
54
+
55
+ Text input: `--text` / `--file` / `--stdin`. Global: `--pretty` for a human,
56
+ `--yes` for destructive verbs, `--dry-run` to preview.
57
+
58
+ **`ADO_ORG`, `ADO_PROJECT` and `ADO_PAT` are already in the environment** after
59
+ setup. Do not check them before working; a missing one announces itself as
60
+ `"kind": "config"` on exit 2, which is the only time it matters.
61
+
62
+ ## Finding the work: `pharos query`
63
+
64
+ **Do not reach for `curl` and the WIQL endpoint.** This skill used to hand you a
65
+ recipe for exactly that, because there was no query verb. There is one now, and
66
+ it does the part the recipe could not: WIQL returns **ids only**, so the recipe
67
+ gave you sixteen bare numbers and a call per item to make them mean anything.
68
+
69
+ ```bash
70
+ pharos query --mine # assigned to you, still open
71
+ pharos query --sprint "Sprint 1" # --sprint current for @currentIteration
72
+ pharos query --type Epic --state Doing # both repeatable
73
+ pharos query --assignee "ada@contoso.com"
74
+ pharos query --tag api --all # --all includes finished work
75
+ pharos query --wiql "SELECT [System.Id] FROM WorkItems WHERE …" # escape hatch
76
+ ```
77
+
78
+ Flags AND together. Output is hydrated items — id, type, title, state, assignee,
79
+ iteration, tags, priority, changed — so `--mine` is one command, not a query
80
+ followed by a fetch per result. Follow up with `pharos task <id>` only for the
81
+ few you are actually going to work on.
82
+
83
+ **Read the `openness` field before you report a count.** "12 open" is
84
+ meaningless until you know what was counted as finished, and Azure DevOps lets a
85
+ process template rename every state. `query` reads the project's own state
86
+ categories and tells you which states it treated as terminal; if it could not
87
+ read them it says so and falls back to guessing, and that is your cue to pass
88
+ `--state` explicitly. `assignedTo` names who `@Me` actually resolved to — a
89
+ shared or service token makes "assigned to me" quietly mean somebody else.
90
+
91
+ Do NOT write `[System.State] NOT IN GROUP 'Completed'` if you reach for `--wiql`.
92
+ It parses, returns 200, and matches **everything** — `IN GROUP` covers work item
93
+ TYPE categories only, and an unknown group resolves to the empty set with no
94
+ error. Measured, 2026-08-05.
95
+
96
+ ## Changing a work item
97
+
98
+ ```bash
99
+ pharos update 225 --state Doing # move the state when the work moves
100
+ pharos update 225 --priority 1 --assignee "ada@contoso.com"
101
+ pharos update 225 --field Microsoft.VSTS.Scheduling.RemainingWork=3
102
+ pharos attach 225 ./bestsellers.xlsx --comment "The numbers"
103
+ ```
104
+
105
+ `update` **reads the item and applies the change under a `test` op on `/rev`**,
106
+ so somebody who wrote between your read and your write gets you a `conflict`
107
+ rather than losing their edit. Setting a value it already has writes nothing and
108
+ says so — a pointless PATCH still bumps `System.Rev` and invalidates every other
109
+ cached revision on the item. `--dry-run` shows the before → after and writes
110
+ nothing. There is no `--yes`: a field edit is an ordinary edit and Azure DevOps
111
+ keeps every revision.
112
+
113
+ `attach` uploads the bytes and then links them as an `AttachedFile` relation —
114
+ two calls, one command. Attachments are **immutable**: attaching the same file
115
+ twice makes two of them, and there is no replace and no versioning. `detach`
116
+ takes one off; it needs `--yes`, finds the attachment by identity rather than by
117
+ position, and leaves the bytes in Azure DevOps so re-attaching the url puts it
118
+ back.
119
+
120
+ `create` puts `--parent` in the same patch as the fields, so a child is never
121
+ briefly an orphan, and it takes the same flag names as `update`.
122
+
123
+ **`unlink` refuses rather than guessing, and that is worth knowing before you
124
+ see it.** Azure DevOps removes a relation by its POSITION in the array, so an
125
+ index from a stale read cuts a different link and the request still succeeds.
126
+ `unlink` finds the relation by identity and removes it under a `test` op on the
127
+ revision it read. If the relation is not there you get exit 3 — that means
128
+ nothing happened, not that the call failed.
129
+
130
+ `delete` moves to the **Recycle Bin** and needs `--yes`; the refusal quotes the
131
+ title first. `restore <id>` brings it back and needs no flag.
132
+
133
+ **There is no permanent delete here, deliberately.** It is the only irreversible
134
+ verb Azure DevOps has, and `--yes` is a flag you have learned to pass. The web
135
+ UI owns it. If somebody genuinely needs to purge, send them there rather than
136
+ looking for a flag.
137
+
138
+ ## Pictures in text, and files beside it
139
+
140
+ **These are two different things and picking the wrong one is the mistake worth
141
+ avoiding.**
142
+
143
+ ```bash
144
+ pharos attach 225 ./bestsellers.xlsx # a FILE, in the Attachments list
145
+ pharos image ./screenshot.png # a PICTURE, to put inside the text
146
+ ```
147
+
148
+ An **attachment** creates an `AttachedFile` relation and appears in the work
149
+ item's Attachments list. An **inline image** creates no relation at all —
150
+ measured on #333, two screenshots pasted into a description gave `relations: 0`
151
+ and `attachments: []`. It is an `![](…)` in the field text and nothing else,
152
+ which is why the Attachments list is right to show nothing for it.
153
+
154
+ So `image` prints a markdown line and leaves the writing to you:
155
+
156
+ ```bash
157
+ pharos image ./chart.png --pretty
158
+ # ![chart](https://dev.azure.com/…/wit/attachments/<guid>?fileName=chart.png)
159
+
160
+ pharos update 225 --field System.Description="$(cat <<'EOF'
161
+ Revenue is up. See the chart:
162
+
163
+ ![chart](https://dev.azure.com/…/wit/attachments/<guid>?fileName=chart.png)
164
+ EOF
165
+ )"
166
+ ```
167
+
168
+ **A wiki picture is a different endpoint and needs `wiki image`.** A wiki is a
169
+ git repository, so an attachment there is a FILE and its NAME is its identity:
170
+ upload a second `image.png` and it lands on the first, and every page pointing
171
+ at `/.attachments/image.png` silently changes picture. `wiki image` makes the
172
+ name unique before sending and prints a repo-relative link:
173
+
174
+ ```bash
175
+ pharos wiki image ./diagram.png --pretty
176
+ # ![diagram](/.attachments/diagram-1786046773042.png)
177
+ ```
178
+
179
+ Use that path, **not** an absolute url — a page linking to a `wit/attachments`
180
+ url renders for anyone with a session and breaks for everybody else. The same
181
+ markdown works in a page and in a page comment.
182
+
183
+ ## Mentioning somebody
184
+
185
+ **A mention is `@<guid>` and nothing else notifies.** `@Ada Lovelace` written
186
+ into a comment is plain text: it reads like a mention to every human who sees
187
+ it, links to nobody, and sends no notification. Nothing errors, so this fails
188
+ silently and stays failed.
15
189
 
16
190
  ```bash
17
- pharos task <id>
191
+ pharos people # everyone the board knows, with the form
192
+ pharos people ada # filter by name or email
193
+ pharos comment add 225 --text "Ready for review @<0f45a818-878d-6d7a-ba8c-1f5568a89ed4>"
18
194
  ```
19
195
 
20
- That returns the work item, its comments, its attachments, its relations **with
21
- their titles**, and the full content of every linked wiki page **plus the
22
- discussion on those pages** in one call.
196
+ The names come from the board's own work items everyone assigned, creating or
197
+ changing anything rather than from an identity endpoint, because those live on
198
+ another host and want scopes a work-scoped PAT does not have. So somebody who
199
+ has never touched an item here will not be listed; the guid out of any Azure
200
+ DevOps url works just as well.
201
+
202
+ ## Turning documents into wiki pages
203
+
204
+ ```bash
205
+ pharos wiki import ./notes.md ./spec.docx --under "/Guides"
206
+ pharos wiki import ./report.pdf --as "Q3 Report"
207
+ ```
23
208
 
24
- **Do this before anything else, and do not assemble it yourself.** By hand it is
25
- five or six lookups across two tools, and the plan a colleague wrote is usually
26
- on a linked wiki page rather than in the description. A task worked without it
27
- is a task worked without the plan.
209
+ `.md` and `.txt` are copied **verbatim** they are already the target format,
210
+ and anything done to them would be reformatting a document somebody wrote
211
+ deliberately. `.docx`, `.pdf`, `.rtf` and `.html` are converted by
212
+ `pharos-convert`, which is the **same converter the macOS app uses**, so both
213
+ produce the same page from the same file. If it is not on PATH, markdown and
214
+ text still import and everything else is refused by name with the reason.
28
215
 
29
- Add `--pretty` when a person will read the output.
216
+ Three rules, because each of them is a way to lose work:
217
+
218
+ - **A name collision SKIPS and says so.** A page write with an empty version is
219
+ a *create*, so writing over an existing page is silent data loss.
220
+ - **Names are settled against the batch as well as the wiki**, so importing
221
+ `Notes.docx` beside `Notes.pdf` gives two pages rather than one written twice.
222
+ A name you give with `--as` is never renamed — it is an instruction, so it is
223
+ allowed to collide and skip.
224
+ - **An empty document is refused.** A scanned PDF carries no extractable text at
225
+ all and PDFKit returns an empty string with no error; an empty page would look
226
+ like a successful import until somebody opened it.
227
+
228
+ Exit 3 when **nothing** landed. A partial batch exits 0 and names what skipped —
229
+ retrying it blindly would collide with the pages it just made.
230
+
231
+ ## Do not guess a state name — ask
232
+
233
+ ```bash
234
+ pharos types # every type, its states, and which mean "finished"
235
+ pharos types --type Task
236
+ ```
237
+
238
+ **`update` now refuses a state the item's type does not have, before writing,
239
+ with the valid ones attached** — so you rarely need to run this first. Run it
240
+ when you want to see the shape, or when composing a `--wiql` filter.
241
+
242
+ **States belong to a TYPE, not to the project.** A state that exists elsewhere
243
+ is still not one this item can take: `In Progress` is real on a Test Suite and
244
+ invalid on a Task, and checking the project as a whole is the same guess one
245
+ layer down. The categories are shown as well as the names because "which
246
+ states exist" and "which mean finished" are different questions — `Inactive` is
247
+ finished on a Test Plan and appears in nobody's hard-coded Done/Closed/Removed
248
+ list.
249
+
250
+ `pharos whoami` is the other one worth reaching for early: it names the identity
251
+ behind `ADO_PAT`, which is who `@Me` resolves to and who every write is
252
+ attributed to. A shared or service token quietly makes "assigned to me" mean
253
+ somebody else. It is org-scoped, so it still answers when the project is
254
+ misconfigured — which is exactly when you need it.
255
+
256
+ ## What `pharos` does NOT do — read this before you go looking
257
+
258
+ - **Free-text and code search.** Nothing here covers it. For work items,
259
+ `pharos query --wiql "… WHERE [System.Title] CONTAINS 'thing'"` gets close;
260
+ for code there is no substitute short of the REST API.
261
+ - **Iterations, areas, capacity, backlogs, teams.**
262
+ - Pull requests, builds, pipelines.
263
+
264
+ **There is no Azure DevOps MCP server here any more, and that is deliberate.**
265
+ It authenticated through the Azure CLI, so it opened a browser mid-task — which
266
+ makes a headless session stop and wait for a human who is not watching.
267
+
268
+ For a **read** this tool does not offer, the REST API is fine and costs nothing
269
+ to get wrong. For a **write** it does not offer, say the gap out loud rather than
270
+ routing around it: several things here exist in no other Azure DevOps tool at
271
+ all — wiki page comments and reactions, attachment upload AND removal, inline
272
+ images for a work item field or a wiki page, mentioning somebody in a form that
273
+ actually notifies, importing a Word document or a PDF as a page, editing or
274
+ deleting a work item comment, service hooks, and applying a change under a
275
+ `test` op on `/rev` so a teammate who wrote first cannot be silently
276
+ overwritten. Those are what the
277
+ guards are, and they are the reason to come back here rather than hand-roll.
278
+
279
+ ## Start every task with one command
280
+
281
+ ```bash
282
+ pharos task <id> # a leaf
283
+ pharos task <id> --children --compact # an EPIC and everything under it
284
+ ```
285
+
286
+ One call: the item, its comments, its attachments, its relations **with their
287
+ titles**, and the full content of every linked wiki page **plus the discussion
288
+ on those pages**.
289
+
290
+ **Do not assemble this yourself.** By hand it is five or six lookups, and the
291
+ plan a colleague wrote is usually on a linked wiki page rather than in the
292
+ description. A task worked without it is a task worked without the plan.
30
293
 
31
294
  Anything that could not be fetched appears in `problems[]`. **If that array is
32
295
  not empty, say so before acting** — a context with an invisible hole in it gets
33
296
  reasoned from confidently.
34
297
 
35
- ## Which tool: `pharos` or the Azure DevOps MCP server
298
+ ### What it actually returns
299
+
300
+ **Read this before writing a parser.** The shapes below are the CLI's, not the
301
+ Azure DevOps API's, and they differ in exactly the places you would guess wrong:
302
+
303
+ ```jsonc
304
+ {
305
+ "id": 40,
306
+ "fields": { // the raw ADO field bag
307
+ "System.Title": "…",
308
+ "System.State": "To Do",
309
+ "System.Description": "plain text, real newlines — NOT html",
310
+ "System.AssignedTo": { "displayName": "…", "uniqueName": "…" }
311
+ },
312
+ "comments": [
313
+ { "id": 1741895,
314
+ "text": "I have updated the description",
315
+ "createdBy": "André Kwakernaat", // a STRING, already flattened
316
+ "createdDate": "2026-08-05T10:15:43.167Z" }
317
+ ],
318
+ "attachments": [],
319
+ "related": [ // titles and states ALREADY resolved
320
+ { "id": 46, "rel": "System.LinkTypes.Hierarchy-Forward",
321
+ "title": "Skill: …", "state": "To Do" }
322
+ ],
323
+ "wikiPages": [],
324
+ "problems": [],
325
+ "children": [] // only with --children/--depth
326
+ }
327
+ ```
36
328
 
37
- If both are available, they overlap. The dividing line:
329
+ Three things that trip up a parser written against the REST API:
38
330
 
39
- | | |
40
- |---|---|
41
- | **Use `pharos`** | anything touching a **wiki page's comments** (the MCP has no tool for these at all), deleting a comment, reactions, deleting a wiki page, and `pharos task <id>` for gathering context |
42
- | **Either works** | reading and querying work items, creating them, updating fields and state, adding a comment, reading a wiki page |
43
- | **The MCP may be better** | code search, repository and pull request work, anything outside work items and wikis |
331
+ - **`comments[].createdBy` is a string**, not an identity object. There is no
332
+ `.displayName` on it.
333
+ - **Description and comment text are plain text**, already converted. Do not
334
+ strip tags; there are none.
335
+ - **A field is ABSENT when unset, not empty.** `fields["System.Description"]` is
336
+ simply missing on an item nobody has written one for — which is a *finding*
337
+ worth reporting, not a crash. Measured: seven of eight children of one epic.
44
338
 
45
- When in doubt use `pharos`: its failure modes are structured and its guards are
46
- explicit.
339
+ `--pretty` renders all of the above as readable markdown and drops the identity
340
+ noise. It is not only "for a human" — for reading a single item it is usually
341
+ the better format for you too.
342
+
343
+ ### Starting from an EPIC
344
+
345
+ An epic is not a big task, and the loop below is written for a leaf. Handed a
346
+ parent, the first question is **which children are actually specified**:
347
+
348
+ ```bash
349
+ pharos task 39 --children --compact
350
+ ```
351
+
352
+ One call instead of one per child — measured on a real epic, nine calls and
353
+ ~42 KB became one call and ~22 KB. Then, **before proposing any work**, report
354
+ the split:
355
+
356
+ > #40 has a complete 5,694-character spec. #41–#47 have no description at all.
357
+ > Seven of eight are placeholders.
358
+
359
+ That is the single most useful thing to say about a parent item, and it is the
360
+ thing an agent is most likely to skip — the titles read like a plan, so an
361
+ implementation gets inferred from them and nobody notices it was invented.
362
+
363
+ `--depth <n>` walks further (max 5). A child that cannot be read becomes a
364
+ `problems[]` entry rather than failing the whole call, so a partial tree still
365
+ tells you what is missing.
366
+
367
+ `--compact` flattens identities to display names and drops the `WEF_…` board
368
+ extension keys. Measured at ~24% of an item's bytes and nothing reads them.
47
369
 
48
370
  ## Reading the outcome
49
371
 
50
- Output is a contract. **Success is JSON on stdout; failure is JSON on stderr
51
- with a non-zero exit.** An empty array with exit 0 is a query that matched
52
- nothing — a different fact from a failure.
372
+ Success is JSON on stdout; failure is JSON on stderr with a non-zero exit. An
373
+ empty array with exit 0 is a query that matched nothing — a different fact from
374
+ a failure.
375
+
376
+ **Read `advice` when it is there.** Azure DevOps answers a licence problem with
377
+ `TF401289: The current user does not have permission to create tag definitions`,
378
+ which is accurate and tells you nothing to do. Where the code is recognised the
379
+ error carries an `advice` field saying what it actually means here — including
380
+ the cases that read as one thing and are another: a failed tag on a create means
381
+ **the work item exists and only the tag is missing**, and a refused delete is
382
+ usually the account's ACCESS LEVEL rather than any permission, because a
383
+ Stakeholder cannot delete however the permissions are set. `credentialIsFine:
384
+ true` means stop re-checking the token.
53
385
 
54
386
  | exit | meaning | what to do |
55
387
  |---|---|---|
56
388
  | `0` | it worked | carry on |
57
389
  | `1` | the call failed | check `kind`; retry only if it is `rateLimit` (wait `retryAfterMs`) or transient |
58
390
  | `2` | called wrong, or not configured | **never retry unchanged.** Fix the call, or the setup |
59
- | `3` | a guard here refused | re-run with `--yes`, or raise `--max-writes` — after deciding it is right |
391
+ | `3` | a guard here refused | re-run with `--yes` — after deciding it is right |
60
392
 
61
393
  `"kind": "conflict"` means somebody wrote first. Your work is still valid:
62
394
  re-read, re-apply. It carries both revisions — and note that **posting a comment
@@ -71,49 +403,51 @@ pharos wiki delete /Plans/Old --yes # done
71
403
  ```
72
404
 
73
405
  **Exit 3 with `"kind": "refused"` means nothing happened.** Do not report the
74
- work as done. The refusal carries a preview of what it would have done — check
75
- that preview is what you intended before adding `--yes`, rather than reflexively
76
- re-running with the flag.
77
-
78
- Replacing a wiki page needs `--yes`; creating one does not. The refusal tells
79
- you how many bytes are at stake, which is how you notice you are about to
80
- overwrite somebody's page instead of writing a new one.
406
+ work as done. The refusal carries a preview check it is what you intended
407
+ before adding `--yes`, rather than reflexively re-running with the flag.
81
408
 
82
- `--dry-run` gives the preview with exit 0 when you want to look without being
83
- refused.
409
+ Replacing a wiki page needs `--yes`; creating one does not. The refusal says how
410
+ many bytes are at stake, which is how you notice you are about to overwrite
411
+ somebody's page instead of writing a new one.
84
412
 
85
- ## The traps are handled. Do not work around them.
413
+ ## Never hand-roll a WRITE
86
414
 
87
- Every known Azure DevOps failure of this kind is handled inside the tool: the
415
+ Every known Azure DevOps trap on the write path is handled inside the tool: the
88
416
  lost-update on wiki writes, the deleted-comment field that lies, the reaction
89
417
  call that needs an empty body, artifact links that must carry a project GUID,
90
418
  relation removal that would otherwise take the wrong link.
91
419
 
92
- **So if something looks like it needs a workaround, it does not.** A `pharos`
93
- command failing means the request was genuinely wrong or the API genuinely
94
- refused — read the error rather than reaching for `curl`. If you find a real gap,
95
- say so plainly; do not paper over it with raw REST calls that skip the guards.
420
+ So a **failing** `pharos` command means the request was genuinely wrong or the
421
+ API genuinely refused. Read the error; do not reach for `curl`.
422
+
423
+ This is a rule about writes, not about reads. A read `pharos` does not offer
424
+ reach for `query --wiql`, or the REST API — is fine, costs nothing to get wrong,
425
+ and is better than refusing to answer. A write it does not offer is a gap worth
426
+ reporting, not routing around: the guards are the reason the tool exists.
427
+
428
+ **Moving or renaming a page changes what it IS.** A wiki page has no id — the
429
+ path is its identity — so anything pointing at the old path stops resolving,
430
+ including artifact links from work items, and nothing reports it. That is why
431
+ both verbs need `--yes` while `write` does not, and why the refusal names what
432
+ it is about to break. Sub-pages move with their parent.
96
433
 
97
434
  ## Three things no tool can fix
98
435
 
99
436
  1. **A wiki is a git repository.** Two writes to the same wiki at the same
100
437
  moment are two pushes racing for one HEAD. Write pages one at a time.
101
438
  2. **There is no wiki event in service hooks.** You cannot subscribe to "a wiki
102
- page changed" — only to `git.push` on the wiki's repository. If you need to
103
- react to wiki edits, that is the only route.
439
+ page changed" — only to `git.push` on the wiki's repository.
104
440
  3. **Wiki ancestors are not created for you.** Writing `/A/B` when `/A` does not
105
441
  exist is refused. Build a page tree top-down, one write per level.
106
442
 
107
443
  ## The loop
108
444
 
109
- Working a task end to end, in the order that keeps the board honest:
110
-
111
445
  1. `pharos task <id>` — read everything, including the linked plan.
112
446
  2. If a plan is needed, write it: `pharos wiki write /Plans/<name> --stdin`,
113
447
  then link it to the epic so the next person finds it the same way you did.
114
448
  3. Break it down — create the child items.
115
449
  4. `pharos comment add <id> --file notes.md` — decisions belong on the item,
116
- not only in a chat log that nobody else can read.
450
+ not only in a chat log nobody else can read.
117
451
  5. Move the state when the work moves, not at the end.
118
452
 
119
453
  **Every person uses their own token.** Board attribution is per-person, so
@@ -122,17 +456,9 @@ sharing one.
122
456
 
123
457
  ## When it is not set up
124
458
 
125
- `"kind": "config"` on exit 2 means the environment is missing. Run:
126
-
127
- ```bash
128
- pharos setup
129
- ```
130
-
131
- It asks for the organisation, project and token, stores the token in the OS
132
- keychain rather than a file, writes the right shell profile, and verifies both
133
- permission scopes — Work Items and Wiki are separate in Azure DevOps, and a
134
- token missing the second one works until the first wiki write.
135
-
136
- Variables live in the environment (`ADO_ORG`, `ADO_PROJECT`, `ADO_PAT`) and
137
- **never in the tool's own config**, which is what lets the same command run in
138
- CI and unattended. After setup, a **new** shell is needed.
459
+ `"kind": "config"` on exit 2 means the environment is missing. Run `pharos
460
+ setup`: it stores the token in the OS keychain rather than a file, writes the
461
+ profile that **scripted** shells read, and verifies both permission scopes —
462
+ Work Items and Wiki are separate in Azure DevOps, and a token missing the second
463
+ works fine until the first wiki write days later. A **new** shell is needed
464
+ afterwards.