@floh-solutions/pharos-cli 0.26.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +214 -146
- package/dist/cli.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +27 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/pr.d.ts +56 -0
- package/dist/commands/pr.d.ts.map +1 -0
- package/dist/commands/pr.js +202 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/search.d.ts +47 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +200 -0
- package/dist/commands/search.js.map +1 -0
- package/package.json +3 -3
- package/skill/SKILL.md +143 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { AdoSearchQueryError } from "@floh-solutions/ado-core";
|
|
2
|
+
import { emit, emitText, usageError } from "../output.js";
|
|
3
|
+
const DEFAULT_TOP = 25;
|
|
4
|
+
export async function runSearch(io, session, rest, options) {
|
|
5
|
+
if (options.itemsOnly && options.wikiOnly) {
|
|
6
|
+
throw usageError("Pass either --items or --wiki, not both. Passing neither searches both, which is the "
|
|
7
|
+
+ "default and almost always what you want.");
|
|
8
|
+
}
|
|
9
|
+
// Every remaining positional is a term, so `pharos search wiki import` works
|
|
10
|
+
// without quoting. Quoting still behaves identically.
|
|
11
|
+
const terms = rest.join(" ").trim();
|
|
12
|
+
if (terms === "") {
|
|
13
|
+
throw usageError('A search needs terms: pharos search "retry backoff". Azure DevOps answers an empty '
|
|
14
|
+
+ "query with 400, so this is refused here rather than paid for as a round trip.");
|
|
15
|
+
}
|
|
16
|
+
const filters = {};
|
|
17
|
+
// PascalCase on the way in, lowercase on the way back — the endpoint's own
|
|
18
|
+
// asymmetry, absorbed in ado-core. See WorkItemSearchOptions.filters.
|
|
19
|
+
if (options.types.length > 0)
|
|
20
|
+
filters["System.WorkItemType"] = options.types;
|
|
21
|
+
if (options.states.length > 0)
|
|
22
|
+
filters["System.State"] = options.states;
|
|
23
|
+
if (options.wikiOnly && (options.types.length > 0 || options.states.length > 0)) {
|
|
24
|
+
throw usageError("--type and --state filter work items, and --wiki excludes work items — so this search "
|
|
25
|
+
+ "would silently ignore them. Drop --wiki, or drop the filters.");
|
|
26
|
+
}
|
|
27
|
+
const top = options.top ?? DEFAULT_TOP;
|
|
28
|
+
const shared = {
|
|
29
|
+
top,
|
|
30
|
+
...(options.skip === undefined ? {} : { skip: options.skip }),
|
|
31
|
+
...(options.org ? { orgWide: true } : {}),
|
|
32
|
+
};
|
|
33
|
+
const problems = [];
|
|
34
|
+
const search = session.client.search;
|
|
35
|
+
const [items, wiki] = await Promise.all([
|
|
36
|
+
options.wikiOnly
|
|
37
|
+
? undefined
|
|
38
|
+
: search
|
|
39
|
+
.workItems(terms, { ...shared, ...(Object.keys(filters).length === 0 ? {} : { filters }) })
|
|
40
|
+
.catch((error) => {
|
|
41
|
+
problems.push(`work item search failed: ${describe(error)}`);
|
|
42
|
+
return undefined;
|
|
43
|
+
}),
|
|
44
|
+
options.itemsOnly
|
|
45
|
+
? undefined
|
|
46
|
+
: search.wiki(terms, shared).catch((error) => {
|
|
47
|
+
problems.push(`wiki search failed: ${describe(error)}`);
|
|
48
|
+
return undefined;
|
|
49
|
+
}),
|
|
50
|
+
]);
|
|
51
|
+
// Both halves refused: that is a failed search, not an empty one. Exit
|
|
52
|
+
// non-zero so a caller checking only the status does not read it as "nothing
|
|
53
|
+
// is written about this anywhere".
|
|
54
|
+
if (items === undefined && wiki === undefined && problems.length > 0) {
|
|
55
|
+
throw usageError(problems.join(" | "));
|
|
56
|
+
}
|
|
57
|
+
const result = {
|
|
58
|
+
terms,
|
|
59
|
+
scope: options.org ? "organization" : "project",
|
|
60
|
+
...(items === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: {
|
|
63
|
+
workItems: {
|
|
64
|
+
count: items.count,
|
|
65
|
+
returned: items.results.length,
|
|
66
|
+
...(items.emptyBecause === undefined ? {} : { emptyBecause: items.emptyBecause }),
|
|
67
|
+
results: items.results.map(workItemSummary),
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
...(wiki === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: {
|
|
73
|
+
wiki: {
|
|
74
|
+
count: wiki.count,
|
|
75
|
+
returned: wiki.results.length,
|
|
76
|
+
...(wiki.emptyBecause === undefined ? {} : { emptyBecause: wiki.emptyBecause }),
|
|
77
|
+
results: wiki.results.map(wikiSummary),
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
// Stated whenever a total exceeds what was returned, because "6 results"
|
|
81
|
+
// and "6 of 97" are different answers and only one of them is honest.
|
|
82
|
+
...(truncationNote(items?.count, items?.results.length, wiki?.count, wiki?.results.length, top) ?? {}),
|
|
83
|
+
problems,
|
|
84
|
+
};
|
|
85
|
+
return session.pretty ? emitText(io, render(result)) : emit(io, result);
|
|
86
|
+
}
|
|
87
|
+
function workItemSummary(result) {
|
|
88
|
+
return {
|
|
89
|
+
id: result.id,
|
|
90
|
+
type: result.type,
|
|
91
|
+
title: result.title,
|
|
92
|
+
state: result.state,
|
|
93
|
+
assignedTo: result.assignedTo,
|
|
94
|
+
tags: result.tags,
|
|
95
|
+
changed: result.changed,
|
|
96
|
+
matched: result.hits.map((hit) => ({ field: hit.field, text: hit.highlights })),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function wikiSummary(result) {
|
|
100
|
+
return {
|
|
101
|
+
path: result.path,
|
|
102
|
+
wiki: result.wiki,
|
|
103
|
+
matched: result.hits.map((hit) => ({ field: hit.field, text: hit.highlights })),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Say so when the page is smaller than the total.
|
|
108
|
+
*
|
|
109
|
+
* Measured: a broad term reports `count: 97` while returning the `$top` asked
|
|
110
|
+
* for. A caller that reports the page size as the answer tells somebody there
|
|
111
|
+
* are three of something when there are ninety-seven.
|
|
112
|
+
*/
|
|
113
|
+
function truncationNote(itemCount, itemReturned, wikiCount, wikiReturned, top) {
|
|
114
|
+
const truncated = (itemCount !== undefined && itemReturned !== undefined && itemCount > itemReturned)
|
|
115
|
+
|| (wikiCount !== undefined && wikiReturned !== undefined && wikiCount > wikiReturned);
|
|
116
|
+
if (!truncated)
|
|
117
|
+
return undefined;
|
|
118
|
+
return {
|
|
119
|
+
more: `Showing the first ${top} of each. "count" is the true total — raise --top or page with `
|
|
120
|
+
+ "--skip to see the rest.",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function describe(error) {
|
|
124
|
+
if (error instanceof AdoSearchQueryError)
|
|
125
|
+
return error.message;
|
|
126
|
+
return error instanceof Error ? error.message : String(error);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The human rendering.
|
|
130
|
+
*
|
|
131
|
+
* The matching line is the point, so it is indented under its result rather
|
|
132
|
+
* than summarised away — a list of page paths answers "how many" when the
|
|
133
|
+
* question was "where".
|
|
134
|
+
*/
|
|
135
|
+
function render(result) {
|
|
136
|
+
const lines = [`"${result.terms}" across the ${result.scope}`, ""];
|
|
137
|
+
if (result.workItems !== undefined) {
|
|
138
|
+
const { count, returned, results } = result.workItems;
|
|
139
|
+
lines.push(`WORK ITEMS — ${count}${count > returned ? ` (showing ${returned})` : ""}`);
|
|
140
|
+
if (results.length === 0) {
|
|
141
|
+
lines.push(` ${result.workItems.emptyBecause ?? "(nothing matched)"}`);
|
|
142
|
+
}
|
|
143
|
+
for (const item of results) {
|
|
144
|
+
lines.push(` #${String(item.id ?? "?").padEnd(6)} ${(item.type ?? "").padEnd(10)} `
|
|
145
|
+
+ `${(item.state ?? "").padEnd(12)} ${item.title ?? "(untitled)"}`);
|
|
146
|
+
for (const line of matchLines(item.matched, (hit, text) => `${hit}: ${text}`)) {
|
|
147
|
+
lines.push(` ${line}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
lines.push("");
|
|
151
|
+
}
|
|
152
|
+
if (result.wiki !== undefined) {
|
|
153
|
+
const { count, returned, results } = result.wiki;
|
|
154
|
+
lines.push(`WIKI — ${count}${count > returned ? ` (showing ${returned})` : ""}`);
|
|
155
|
+
if (results.length === 0) {
|
|
156
|
+
lines.push(` ${result.wiki.emptyBecause ?? "(nothing matched)"}`);
|
|
157
|
+
}
|
|
158
|
+
for (const page of results) {
|
|
159
|
+
lines.push(` ${page.path ?? "(unknown path)"}`);
|
|
160
|
+
for (const line of matchLines(page.matched, (_hit, text) => text)) {
|
|
161
|
+
lines.push(` ${line}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
lines.push("");
|
|
165
|
+
}
|
|
166
|
+
if (result.more !== undefined)
|
|
167
|
+
lines.push(result.more);
|
|
168
|
+
if (result.problems.length > 0) {
|
|
169
|
+
lines.push("", "problems:");
|
|
170
|
+
for (const problem of result.problems)
|
|
171
|
+
lines.push(` - ${problem}`);
|
|
172
|
+
}
|
|
173
|
+
return lines.join("\n").trimEnd();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* At most {@link SHOWN_PER_RESULT} matching lines per result, and it SAYS when
|
|
177
|
+
* it dropped some.
|
|
178
|
+
*
|
|
179
|
+
* A single work item can match on a dozen passages across its description and
|
|
180
|
+
* its discussion, and one result then fills a screen. The JSON keeps every one
|
|
181
|
+
* of them — this cap is the human rendering only. It is stated rather than
|
|
182
|
+
* silent: a reader who cannot tell a complete list from a truncated one has to
|
|
183
|
+
* re-run the search to find out.
|
|
184
|
+
*/
|
|
185
|
+
const SHOWN_PER_RESULT = 4;
|
|
186
|
+
function matchLines(matched, format) {
|
|
187
|
+
const all = matched.flatMap((hit) => hit.text.map((text) => format(hit.field, oneLine(text))));
|
|
188
|
+
if (all.length <= SHOWN_PER_RESULT)
|
|
189
|
+
return all;
|
|
190
|
+
return [
|
|
191
|
+
...all.slice(0, SHOWN_PER_RESULT),
|
|
192
|
+
`… and ${all.length - SHOWN_PER_RESULT} more matching lines (the JSON output has all of them)`,
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
/** Highlights carry real newlines; a match is one line in a list. */
|
|
196
|
+
function oneLine(text) {
|
|
197
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
198
|
+
return flat.length > 160 ? `${flat.slice(0, 157)}…` : flat;
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/commands/search.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floh-solutions/pharos-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Azure DevOps from a headless shell, for an agent: the whole context of a task in one call, and the wiki/comment verbs Microsoft's MCP server does not ship.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "FLOH Solutions",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"node": ">=22"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@floh-solutions/ado-core": "0.
|
|
28
|
-
"@floh-solutions/gh-core": "0.
|
|
27
|
+
"@floh-solutions/ado-core": "0.11.0",
|
|
28
|
+
"@floh-solutions/gh-core": "0.3.0",
|
|
29
29
|
"@floh-solutions/plan-to-board": "0.1.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pharos
|
|
3
|
-
description: "Use for AZURE DEVOPS work — when the user says Azure DevOps, ADO or dev.azure.com, or names an ADO work item by number (\"pick up 4821\"
|
|
3
|
+
description: "Use for AZURE DEVOPS work — when the user says Azure DevOps, ADO or dev.azure.com, or names an ADO work item by number (\"pick up 4821\"). Covers reading or updating a work item, bug, story or epic; the board, backlog, sprint or iteration; what is assigned to you; reading or writing a wiki page and its comments; searching the board and wiki for a phrase; linking a plan to an epic; attaching a file to a work item or taking one off; inline images in a field or page; @mentioning somebody so they are notified; importing Markdown, Word or PDF as pages; and whether a call is retryable. ALSO the GITHUB ISSUE ↔ work item EDGE: adopting an issue as a work item linked at both ends, bulk-adopting a repo, one reply reaching both, where they drifted, closing both ends together, a PULL REQUEST and its work item, and changing a comment that ALREADY EXISTS: editing, deleting, reacting, hiding, pinning. NOT a GitHub CLI: listing or viewing an issue, or POSTING a comment, is `gh`'s job. NOT for other trackers — Argus, Jira, Linear."
|
|
4
4
|
license: Proprietary
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -17,6 +17,12 @@ whoami who ADO_PAT belongs to. @Me resolves to this
|
|
|
17
17
|
types what --state and --type will ACCEPT, per type
|
|
18
18
|
query WHICH work items — assigned to you, in a sprint,
|
|
19
19
|
of a type, still open. Hydrated items, not ids.
|
|
20
|
+
search <terms...> WHERE something is WRITTEN — full text across
|
|
21
|
+
work items AND wiki pages, in one call. Reaches
|
|
22
|
+
descriptions, discussion and page bodies, which
|
|
23
|
+
`query` cannot. Each result carries the MATCHING
|
|
24
|
+
LINE. --items / --wiki to narrow, --org for the
|
|
25
|
+
whole organisation, --top (default 25)
|
|
20
26
|
task <id> one work item, whole: fields, comments,
|
|
21
27
|
attachments, relations WITH titles, and the
|
|
22
28
|
content + discussion of every linked wiki page
|
|
@@ -78,6 +84,13 @@ issue say <owner/name#45> ONE message, TWO audiences: the whole of it to
|
|
|
78
84
|
comment's URL to the board. --summary
|
|
79
85
|
issue trail <id|owner/name#45> the whole trail from EITHER end, with the
|
|
80
86
|
evidence for each half. Read-only
|
|
87
|
+
pr <owner/name#123> a PULL REQUEST and the work it belongs to:
|
|
88
|
+
state, draft, review verdict, WHICH CHECKS ARE
|
|
89
|
+
RED, the issues it closes, and the work items it
|
|
90
|
+
reaches — via AB#123 in the body OR via an
|
|
91
|
+
adopted issue named by "Fixes #45". Also where
|
|
92
|
+
the pull request and the board DISAGREE
|
|
93
|
+
--no-work-items skip the transitive lookup
|
|
81
94
|
issue drift where the two platforms DISAGREE — the report no
|
|
82
95
|
other tool can produce. --repo --limit --wiql
|
|
83
96
|
issue backfill <owner/name> bulk adopt every issue the board does not link
|
|
@@ -188,6 +201,135 @@ It parses, returns 200, and matches **everything** — `IN GROUP` covers work it
|
|
|
188
201
|
TYPE categories only, and an unknown group resolves to the empty set with no
|
|
189
202
|
error. Measured, 2026-08-05.
|
|
190
203
|
|
|
204
|
+
## After adoption, the work becomes code: `pharos pr`
|
|
205
|
+
|
|
206
|
+
`issue trail` follows filing -> adoption -> close. It stops at the moment the
|
|
207
|
+
work becomes a branch. `pharos pr` is the part that does not.
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
pharos pr contoso/widgets#123 # the pull request AND the board
|
|
211
|
+
pharos pr contoso/widgets#123 --pretty
|
|
212
|
+
pharos pr contoso/widgets#123 --no-work-items # skip the transitive lookup
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
One call replaces `gh pr view` + `gh pr checks` + a WIQL query + a relations
|
|
216
|
+
read, across two credentials — and answers one question none of them can.
|
|
217
|
+
|
|
218
|
+
**Two paths reach a work item, and the implicit one is the one that fires:**
|
|
219
|
+
|
|
220
|
+
| | |
|
|
221
|
+
|---|---|
|
|
222
|
+
| `AB#4821` in the body | explicit. What Azure Boards' own app reads |
|
|
223
|
+
| `Fixes #45`, where #45 is adopted | **the one that actually happens** |
|
|
224
|
+
|
|
225
|
+
The second costs one read per closed issue (its comments, where the `pharos:v1`
|
|
226
|
+
trailer lives) and is why `--no-work-items` exists for when you do not need it.
|
|
227
|
+
|
|
228
|
+
### `disagreements` is the part nothing else can produce
|
|
229
|
+
|
|
230
|
+
```jsonc
|
|
231
|
+
{
|
|
232
|
+
"pullRequest": { "number": 123, "state": "MERGED", "merged": true,
|
|
233
|
+
"review": "APPROVED", "checks": "FAILURE",
|
|
234
|
+
"failingChecks": [ { "name": "e2e-tests", "conclusion": "FAILURE" } ] },
|
|
235
|
+
"closesIssues": [45],
|
|
236
|
+
"workItems": [ { "id": 4821, "type": "Issue", "state": "To Do", "title": "…" } ],
|
|
237
|
+
"disagreements": [
|
|
238
|
+
"#4821 is \"To Do\" but this pull request is already MERGED — the work shipped and the board never moved."
|
|
239
|
+
],
|
|
240
|
+
"problems": []
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Azure DevOps cannot see the pull request and GitHub does not know the work item
|
|
245
|
+
exists, so **neither system will ever report this**. An empty `disagreements`
|
|
246
|
+
means they agree; it is silent when there is nothing to say.
|
|
247
|
+
|
|
248
|
+
Three things about the shape:
|
|
249
|
+
|
|
250
|
+
- **`checks` is the rollup and it is authoritative. `failingChecks` names the
|
|
251
|
+
red ones** — found by filtering every context, not by showing the first few.
|
|
252
|
+
Measured: real pull requests report a `FAILURE` rollup while their first
|
|
253
|
+
several contexts all read `SUCCESS`, because the red one is further down.
|
|
254
|
+
- **`NONE` is not `SUCCESS`.** A pull request with no checks configured reports
|
|
255
|
+
`NONE`, and a still-running one is `PENDING`, not a failure.
|
|
256
|
+
- **`state` has three values** — `OPEN`, `CLOSED`, `MERGED`. Merged is its own
|
|
257
|
+
state, not a kind of closed, and `merged: true` is the field to test.
|
|
258
|
+
|
|
259
|
+
Posting a comment on a pull request is still `gh`'s job, exactly as it is for an
|
|
260
|
+
issue. This verb reads; it does not write.
|
|
261
|
+
|
|
262
|
+
## Finding where it is WRITTEN: `pharos search`
|
|
263
|
+
|
|
264
|
+
`query` answers *which work items*. `search` answers *where has this been
|
|
265
|
+
written about* — and they are not the same question.
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
pharos search "retry backoff" # work items AND wiki, one call
|
|
269
|
+
pharos search retry backoff # quoting is optional
|
|
270
|
+
pharos search "sprint policy" --wiki # pages only
|
|
271
|
+
pharos search "flaky" --items --type Bug # work items only, filtered
|
|
272
|
+
pharos search "onboarding" --org # the whole organisation
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
**This is the only way to reach text nobody linked.** `pharos task <id>` returns
|
|
276
|
+
the content of every wiki page **linked** to the item — and you can only follow
|
|
277
|
+
a link somebody already made. A design written on a page and never attached to
|
|
278
|
+
the work item is invisible to every other verb here. That is what this finds.
|
|
279
|
+
|
|
280
|
+
It is also full text. `query --wiql … CONTAINS` matches **titles**; this matches
|
|
281
|
+
descriptions, the discussion, and page bodies.
|
|
282
|
+
|
|
283
|
+
### What it returns
|
|
284
|
+
|
|
285
|
+
```jsonc
|
|
286
|
+
{
|
|
287
|
+
"terms": "retry backoff",
|
|
288
|
+
"scope": "project", // or "organization" with --org
|
|
289
|
+
"workItems": {
|
|
290
|
+
"count": 97, // the TRUE TOTAL, not what was returned
|
|
291
|
+
"returned": 25,
|
|
292
|
+
"results": [
|
|
293
|
+
{ "id": 373, "type": "Issue", "state": "Doing",
|
|
294
|
+
"title": "…", "assignedTo": "Ada Lovelace", "tags": ["api"],
|
|
295
|
+
"matched": [ // WHY this matched — the line itself
|
|
296
|
+
{ "field": "System.Description",
|
|
297
|
+
"text": ["the backoff doubles each attempt"] } ] }
|
|
298
|
+
]
|
|
299
|
+
},
|
|
300
|
+
"wiki": {
|
|
301
|
+
"count": 8, "returned": 8,
|
|
302
|
+
"results": [
|
|
303
|
+
{ "path": "/Roadmap.md", "wiki": "Contoso.wiki",
|
|
304
|
+
"matched": [ { "field": "content", "text": ["…one page per feature."] } ] }
|
|
305
|
+
]
|
|
306
|
+
},
|
|
307
|
+
"more": "Showing the first 25 of each…", // only when count > returned
|
|
308
|
+
"problems": []
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Four things about that shape, because guessing any of them costs a failed parse:
|
|
313
|
+
|
|
314
|
+
- **`count` is the total, `returned` is what you got.** They differ constantly —
|
|
315
|
+
a broad term reports 97 and hands you 25. Reporting `count` as "results I can
|
|
316
|
+
see" is wrong; reporting `returned` as the total is worse.
|
|
317
|
+
- **`matched[].text` is plain text.** The API wraps hits in `<highlighthit>`
|
|
318
|
+
markers; they are stripped before you see them.
|
|
319
|
+
- **Field names are normalised to `System.Description`**, the casing everything
|
|
320
|
+
else here uses. The endpoint itself answers `system.description`.
|
|
321
|
+
- **A surface that failed lands in `problems[]`** and the other still returns —
|
|
322
|
+
a token that reads the wiki but not work items gets the wiki half. If
|
|
323
|
+
`problems` is non-empty, say so before reasoning from the result. Both halves
|
|
324
|
+
failing is a non-zero exit, not an empty answer.
|
|
325
|
+
|
|
326
|
+
### Code search is NOT here
|
|
327
|
+
|
|
328
|
+
It needs the Code Search extension installed on the organisation, which a token
|
|
329
|
+
cannot grant. Asking for it would return an empty result that means "not
|
|
330
|
+
provisioned" rather than "no matches", so the verb does not offer it at all.
|
|
331
|
+
`pharos doctor` reports whether this organisation has it.
|
|
332
|
+
|
|
191
333
|
## Changing a work item
|
|
192
334
|
|
|
193
335
|
```bash
|