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