@floh-solutions/pharos-cli 0.23.0 → 0.25.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 +409 -5
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +23 -0
- package/dist/capabilities.js.map +1 -1
- package/dist/cli.d.ts +4 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +362 -36
- package/dist/cli.js.map +1 -1
- package/dist/commands/comment-github.d.ts +138 -0
- package/dist/commands/comment-github.d.ts.map +1 -0
- package/dist/commands/comment-github.js +427 -0
- package/dist/commands/comment-github.js.map +1 -0
- package/dist/commands/comment.d.ts.map +1 -1
- package/dist/commands/comment.js +114 -19
- package/dist/commands/comment.js.map +1 -1
- package/dist/commands/doctor.d.ts +71 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +414 -8
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/issue.d.ts +75 -0
- package/dist/commands/issue.d.ts.map +1 -0
- package/dist/commands/issue.js +2466 -0
- package/dist/commands/issue.js.map +1 -0
- package/dist/commands/setup.d.ts +6 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +114 -0
- package/dist/commands/setup.js.map +1 -1
- package/dist/half-link.d.ts +157 -0
- package/dist/half-link.d.ts.map +1 -0
- package/dist/half-link.js +257 -0
- package/dist/half-link.js.map +1 -0
- package/dist/issue-scan.d.ts +383 -0
- package/dist/issue-scan.d.ts.map +1 -0
- package/dist/issue-scan.js +455 -0
- package/dist/issue-scan.js.map +1 -0
- package/dist/output.d.ts +18 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +293 -0
- package/dist/output.js.map +1 -1
- package/dist/session.d.ts +101 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +132 -11
- package/dist/session.js.map +1 -1
- package/dist/target.d.ts +40 -4
- package/dist/target.d.ts.map +1 -1
- package/dist/target.js +14 -3
- package/dist/target.js.map +1 -1
- package/package.json +3 -2
- package/skill/SKILL.md +422 -11
package/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pharos
|
|
3
|
-
description: "Use for AZURE DEVOPS work
|
|
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\", \"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; reading or writing an ADO wiki page and its comments; linking a plan to an epic; attaching a file to a work item or taking one off; inline images in a field or wiki page; @mentioning somebody so they are notified; importing Markdown, Word or PDF as wiki pages; and whether a failed 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 reporter and board, where they have drifted, closing both ends together, 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
|
|
|
@@ -59,21 +59,54 @@ wiki image <file> a picture for a PAGE or page comment. Different
|
|
|
59
59
|
wiki import <file...> .md .txt .docx .pdf .rtf .html -> pages. A
|
|
60
60
|
.pptx is refused, on purpose — see below
|
|
61
61
|
--under <path> --as <name>
|
|
62
|
-
comment list | add | edit | delete <target> is a work item id
|
|
63
|
-
comment react | unreact | reactors
|
|
62
|
+
comment list | add | edit | delete <target> is a work item id, a wiki path,
|
|
63
|
+
comment react | unreact | reactors OR a GitHub issue (contoso/widgets#45)
|
|
64
|
+
comment hide | unhide | pin | unpin GITHUB ONLY — Azure DevOps has neither
|
|
65
|
+
REACTIONS ARE NOT ONE VOCABULARY. Azure DevOps:
|
|
66
|
+
like dislike heart hooray smile confused. GitHub:
|
|
67
|
+
thumbs-up thumbs-down laugh hooray confused heart
|
|
68
|
+
rocket eyes. Passing one platform's name to the
|
|
69
|
+
other is refused, not translated
|
|
70
|
+
`comment add` is REFUSED on a GitHub issue — see
|
|
71
|
+
"Editing a GitHub comment" below
|
|
72
|
+
issue adopt <owner/name#45> a GitHub ISSUE becomes a work item, with BOTH
|
|
73
|
+
ends of the link written. --type --title --parent
|
|
74
|
+
issue link <owner/name#45> <id> join an existing pair — or finish a join that
|
|
75
|
+
half-happened. Only the missing half is written
|
|
76
|
+
issue say <owner/name#45> ONE message, TWO audiences: the whole of it to
|
|
77
|
+
the reporter on GitHub, a summary and that
|
|
78
|
+
comment's URL to the board. --summary
|
|
79
|
+
issue trail <id|owner/name#45> the whole trail from EITHER end, with the
|
|
80
|
+
evidence for each half. Read-only
|
|
81
|
+
issue drift where the two platforms DISAGREE — the report no
|
|
82
|
+
other tool can produce. --repo --limit --wiql
|
|
83
|
+
issue backfill <owner/name> bulk adopt every issue the board does not link
|
|
84
|
+
yet. Needs --yes. --limit --state --label
|
|
85
|
+
issue close <id|owner/name#45> close BOTH ends, then RE-READ both to say what
|
|
86
|
+
actually moved. --to --reason --all
|
|
87
|
+
issue edit <owner/name#45> the issue's OWN fields, behind a lost-update
|
|
88
|
+
guard. Needs --yes. --title --text/--file/--stdin
|
|
89
|
+
--milestone --add-label --remove-label
|
|
90
|
+
--add-assignee --remove-assignee --if-title
|
|
91
|
+
--if-body. `gh issue edit` is the UNGUARDED one
|
|
64
92
|
hooks list | check | create | repoint | delete service hooks for realtime.
|
|
65
93
|
`check` needs --hub <url>; `list` shows the URL
|
|
66
94
|
already in use
|
|
67
95
|
plan <file> an implementation plan → a work item tree
|
|
68
96
|
setup org, project, token → keychain + shell profile
|
|
69
97
|
--install ask|all|a,b offer the optional
|
|
70
|
-
capabilities (LibreOffice, poppler, converter
|
|
98
|
+
capabilities (LibreOffice, poppler, converter,
|
|
99
|
+
the GitHub CLI)
|
|
100
|
+
--repo owner/name --gh-account <login> bind a
|
|
101
|
+
repository to the GitHub account that reaches
|
|
102
|
+
it. BOTH or NEITHER — `issue` needs this first
|
|
71
103
|
doctor what is installed on THIS machine, what is
|
|
72
104
|
missing, and what each missing thing costs
|
|
73
105
|
```
|
|
74
106
|
|
|
75
107
|
Text input: `--text` / `--file` / `--stdin`. Global: `--pretty` for a human,
|
|
76
|
-
`--yes` for destructive verbs, `--dry-run` to preview
|
|
108
|
+
`--yes` for destructive verbs, `--dry-run` to preview, `--max-writes <n>` to
|
|
109
|
+
change the per-invocation write cap (default 20; `0` is read-only).
|
|
77
110
|
|
|
78
111
|
**`ADO_ORG`, `ADO_PROJECT` and `ADO_PAT` are already in the environment** after
|
|
79
112
|
setup. Do not check them before working; a missing one announces itself as
|
|
@@ -83,8 +116,10 @@ setup. Do not check them before working; a missing one announces itself as
|
|
|
83
116
|
commands further down name them with confidence they have not earned on yours.
|
|
84
117
|
`pharos doctor` answers that in one call: what is present, its version and where
|
|
85
118
|
it resolved, what is missing, and what each missing thing actually costs. It is
|
|
86
|
-
read-only
|
|
87
|
-
|
|
119
|
+
read-only and works before anything is configured — so it is also the right
|
|
120
|
+
first move when something behaves oddly. Offline too, with one exception it
|
|
121
|
+
names: `gh auth status` validates every token against github.com and there is
|
|
122
|
+
no flag that stops it, so that probe runs only when `gh` is installed.
|
|
88
123
|
|
|
89
124
|
`pharos setup --install ask` then offers to install them, driving Homebrew, apt
|
|
90
125
|
or winget rather than vendoring anything. **Nothing installs without that flag**
|
|
@@ -596,6 +631,345 @@ leading slash**. Two things follow:
|
|
|
596
631
|
website, that is why. `wiki links` and `unlink` both ask about **both** shapes,
|
|
597
632
|
so old links still resolve and can still be removed.
|
|
598
633
|
|
|
634
|
+
## GitHub issues: the join, and only the join
|
|
635
|
+
|
|
636
|
+
**These verbs are newer than the CLI on most machines. Check before you promise
|
|
637
|
+
one.** `pharos issue` shipped *after* **0.23.0**, so if `pharos --version` prints
|
|
638
|
+
0.23.0 or lower it is not there and every verb below is an unknown command —
|
|
639
|
+
`npm i -g @floh-solutions/pharos-cli@latest` is the fix. This skill and the CLI
|
|
640
|
+
update by different routes and drift apart in both directions; `pharos doctor`
|
|
641
|
+
prints both versions.
|
|
642
|
+
|
|
643
|
+
**This is not a GitHub CLI and must not become one.** Listing, viewing and
|
|
644
|
+
plainly commenting on an issue is `gh`'s job and `gh` is better at it; closing
|
|
645
|
+
one *end* is `gh issue close`. Pharos owns the one thing `gh` cannot see: which
|
|
646
|
+
work item tracks this issue, written so it survives in both databases.
|
|
647
|
+
|
|
648
|
+
The one place that line moved: **a comment that already exists** is reachable
|
|
649
|
+
through `pharos comment <verb> <owner/name#45>` — edit, delete, react, unreact,
|
|
650
|
+
reactors, hide, unhide, pin, unpin. Two reasons, and neither is symmetry for its
|
|
651
|
+
own sake. Editing or deleting one can destroy the link record, so it needs the
|
|
652
|
+
guard below rather than a bare `gh api`. And hiding or pinning has **no
|
|
653
|
+
first-class `gh` verb at all** — they are GraphQL mutations with no REST route,
|
|
654
|
+
which is the "nothing else can do this" test the rest of these verbs pass.
|
|
655
|
+
Posting a *new* comment is still not ours.
|
|
656
|
+
|
|
657
|
+
The second place it moved, and it is the only verb here that overlaps `gh` head
|
|
658
|
+
on: **`pharos issue edit` exists to REFUSE.** `gh issue edit` does the same
|
|
659
|
+
write and does it well — what it cannot do is notice that somebody changed the
|
|
660
|
+
thing you were overwriting. It reads nothing and compares nothing, so if a
|
|
661
|
+
colleague renamed the issue while you were composing, their rename is gone and
|
|
662
|
+
neither of you is told. That is fine for a human at a terminal and wrong for an
|
|
663
|
+
agent, which reads an issue, spends a minute thinking, and writes back into a
|
|
664
|
+
world that moved. **If you want an unguarded edit, use `gh issue edit` — it is
|
|
665
|
+
right there and it is better at it.**
|
|
666
|
+
|
|
667
|
+
Azure DevOps has its own GitHub integration and **the link it makes carries
|
|
668
|
+
nothing** — title, body, comments, labels and state stay on their own island,
|
|
669
|
+
and the transition only ever fires from a commit or a PR merge, never from
|
|
670
|
+
closing an issue. That gap is the whole reason these verbs exist.
|
|
671
|
+
|
|
672
|
+
```bash
|
|
673
|
+
pharos issue adopt contoso/widgets#45 --parent 39 # an issue → a work item, both ends
|
|
674
|
+
pharos issue link contoso/widgets#45 4821 # join a pair that already exists
|
|
675
|
+
pharos issue trail 4821 # …or trail contoso/widgets#45
|
|
676
|
+
pharos issue say contoso/widgets#45 --file reply.md
|
|
677
|
+
pharos issue drift # where the two disagree
|
|
678
|
+
pharos issue backfill contoso/widgets --limit 25 --yes
|
|
679
|
+
pharos issue close 4821 --text "Shipped in 1.4.0."
|
|
680
|
+
pharos issue edit contoso/widgets#45 --title "Crash on resize" --yes
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Editing an issue, and the guard GitHub does not give us
|
|
684
|
+
|
|
685
|
+
**There is no conditional write.** `PATCH /repos/{o}/{r}/issues/{n}` answers
|
|
686
|
+
**400** to `If-Match` — measured, not assumed — so the `{"op":"test","path":
|
|
687
|
+
"/rev"}` guarantee behind every Azure DevOps patch has no GitHub equivalent.
|
|
688
|
+
Never send `If-Match` here yourself either: it is not inert, it fails the write,
|
|
689
|
+
and the 400 reads like a malformed body.
|
|
690
|
+
|
|
691
|
+
**And do not guard on `updated_at` or the ETag.** Both move when somebody merely
|
|
692
|
+
*comments* on the issue, so a guard keyed on either refuses a good edit for a
|
|
693
|
+
change that touched nothing — it fires on the case that is *not* a collision.
|
|
694
|
+
|
|
695
|
+
So the guard compares the **value of the field you are overwriting**: re-read
|
|
696
|
+
immediately before sending, and refuse only if that text moved. A comment cannot
|
|
697
|
+
cause it, which is what makes the refusal worth reading.
|
|
698
|
+
|
|
699
|
+
| you pass | the base is | what it protects |
|
|
700
|
+
|---|---|---|
|
|
701
|
+
| nothing | this command's own read | one round trip. Honest, and small |
|
|
702
|
+
| `--if-title` / `--if-body` / `--if-body-file` | what **you** saw | the whole gap between your read and your write |
|
|
703
|
+
|
|
704
|
+
Pass `--if-*` whenever you read the issue and then thought about it — that gap is
|
|
705
|
+
where a collision actually happens. The output says which window it guarded, as
|
|
706
|
+
`fields.guard.window`: `in-command` or `caller`.
|
|
707
|
+
|
|
708
|
+
A refusal is `kind: "conflict"`, exit **1**, and carries `conflicts` (base,
|
|
709
|
+
remote and proposed for each field) plus `attribution` naming who renamed it.
|
|
710
|
+
Re-read, fold their change into yours, edit again with `--if-title` set to what
|
|
711
|
+
you just read. **It is not atomic and nothing client-side can make it so** — it
|
|
712
|
+
narrows the race to one round trip rather than closing it.
|
|
713
|
+
|
|
714
|
+
Two things GitHub does *quietly*, which this verb reports and a bare `gh api`
|
|
715
|
+
would not:
|
|
716
|
+
|
|
717
|
+
| endpoint | the silence |
|
|
718
|
+
|---|---|
|
|
719
|
+
| `POST …/labels` | **creates** a repository label that does not exist — 200, grey, permanent, spelled exactly as you typed it. So an unknown label is REFUSED with the near misses named; `--create-label` is how you mean it |
|
|
720
|
+
| `POST …/assignees` | **ignores** a login it will not assign and still answers 201. Read `assignees.ignored` — a status code is not the answer here |
|
|
721
|
+
|
|
722
|
+
Labels and assignees go through those add/remove endpoints and never through the
|
|
723
|
+
whole-issue PATCH, which carries them as *whole arrays* — one label write would
|
|
724
|
+
otherwise blindly overwrite every label on the issue.
|
|
725
|
+
|
|
726
|
+
### Bind the repository first, or none of this runs
|
|
727
|
+
|
|
728
|
+
```bash
|
|
729
|
+
pharos setup --repo contoso/widgets --gh-account alisina-tibata
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
**Both flags or neither**, and there is deliberately **no fallback to whichever
|
|
733
|
+
account `gh` has active**: two accounts can each see a repository of the same
|
|
734
|
+
name, so the wrong one answers **200** for a different repository — a wrong
|
|
735
|
+
answer rather than an error.
|
|
736
|
+
|
|
737
|
+
An unbound repository is `"kind": "config"` on exit 2, naming
|
|
738
|
+
`~/.config/pharos/repos.json` — never a guess — **and it lists the repositories
|
|
739
|
+
that ARE bound**, so a typo is visible without reading the file. That check runs
|
|
740
|
+
before everything else here, so it is the first thing to fix. `gh` itself is
|
|
741
|
+
optional: without it every `issue` verb is refused by name with the reason and
|
|
742
|
+
the Azure DevOps verbs are untouched. `pharos doctor` reports both.
|
|
743
|
+
|
|
744
|
+
A failure that came from GitHub says **`"platform": "github"`**, because these
|
|
745
|
+
verbs touch two platforms in one call and which one refused is the first thing
|
|
746
|
+
to know. Its absence means Azure DevOps or this tool, as everywhere else.
|
|
747
|
+
|
|
748
|
+
### `adopt` is safe to re-run. `say` is NOT. Do not generalise from one.
|
|
749
|
+
|
|
750
|
+
**Adopting the same issue twice never creates a second work item.** What it
|
|
751
|
+
does instead depends on the state the pair is in, and the exit code tells you
|
|
752
|
+
which:
|
|
753
|
+
|
|
754
|
+
| exit **0**, `created: false` | the issue carries a marker. You get the work item it names and nothing is written — the state you asked for already holds |
|
|
755
|
+
| exit **3**, `halfLinked: true` | a work item already links this issue and the issue says nothing about it. Refused, carrying `recover` — the link wants **finishing**, not repeating |
|
|
756
|
+
| exit **3**, `workItemIds` with two entries | two work items claim one issue. Refused, both named, no `recover`: picking one would be inventing an answer |
|
|
757
|
+
|
|
758
|
+
Azure DevOps is written **first**, because the work item id does not exist until
|
|
759
|
+
the create lands, so the one failure `adopt` can leave is always the same shape:
|
|
760
|
+
the board end written, the GitHub end not.
|
|
761
|
+
|
|
762
|
+
```jsonc
|
|
763
|
+
{ "error": { "createdWorkItem": 4821, "halfLinked": true, "wrote": ["hyperlink"],
|
|
764
|
+
"recover": "pharos issue link contoso/widgets#45 4821" } }
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
**Read more than `kind` on that error**: a work item now exists, and reading
|
|
768
|
+
only the kind loses its id. `recover` is the literal command that finishes the
|
|
769
|
+
job, and nothing needs undoing first.
|
|
770
|
+
|
|
771
|
+
**Re-running `adopt` there is safe but it is not the repair** — it refuses,
|
|
772
|
+
because the row above is exactly the state it detects, and the refusal hands
|
|
773
|
+
back the same `recover` string. Take it either from the failure or from the
|
|
774
|
+
refusal; they are the same command. `link` writes only the half that is missing,
|
|
775
|
+
which makes it both the fix here and the ordinary way to join a pair that
|
|
776
|
+
already exists. `pharos issue drift` finds this state later if nobody acted on
|
|
777
|
+
it at the time — it reports it as `one-sided`, carrying the same repair.
|
|
778
|
+
|
|
779
|
+
Every `adopt` reports `boardCheck`. `{"ran": true}` means the board was searched
|
|
780
|
+
for a work item that already tracks this issue; `{"ran": false}` carries the
|
|
781
|
+
reason it could not be, and then a half-link made on **another machine** would
|
|
782
|
+
not have been seen — `pharos issue drift --repo <owner/name>` asks the same
|
|
783
|
+
question deliberately.
|
|
784
|
+
|
|
785
|
+
`say` is the opposite, and that is the trap. **GitHub is written first there**,
|
|
786
|
+
because the board's comment carries the GitHub comment's URL and that does not
|
|
787
|
+
exist until the POST returns. So a failure on the board half leaves a **public
|
|
788
|
+
comment already posted**, and re-running posts a second one:
|
|
789
|
+
|
|
790
|
+
```jsonc
|
|
791
|
+
{ "error": { "halfSaid": true, "wrote": ["github-comment"],
|
|
792
|
+
"github": { "commentUrl": "https://github.com/…#issuecomment-950" },
|
|
793
|
+
"recover": "pharos comment add 4821 --stdin", "adoComment": "…" } }
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
Feed `adoComment` into `recover`. Do not re-run `say`.
|
|
797
|
+
|
|
798
|
+
### `say` — one message, two audiences
|
|
799
|
+
|
|
800
|
+
| | GitHub | Azure DevOps |
|
|
801
|
+
|---|---|---|
|
|
802
|
+
| who reads it | the reporter, who has **no** Azure DevOps account and never will | the team |
|
|
803
|
+
| what they get | the full explanation | a summary, and the URL of the comment carrying the rest |
|
|
804
|
+
|
|
805
|
+
**The asymmetry is the verb.** The same words in both places is `gh issue
|
|
806
|
+
comment` followed by `pharos comment add`, and neither of those knows the other
|
|
807
|
+
happened. Without `--summary` the board gets the first paragraph cut to 200
|
|
808
|
+
characters — a guess, and allowed to be one *only* because the comment's URL
|
|
809
|
+
travels with it. GitHub is the record of what was said; the board never is.
|
|
810
|
+
|
|
811
|
+
**Never pass a message that already carries a `<!-- pharos:v1 … -->` trailer.**
|
|
812
|
+
It is refused (exit 3) before any call, on `say` and on `close`'s closing note
|
|
813
|
+
alike. That trailer means the text is Pharos's own output coming back round — a
|
|
814
|
+
comment you read and re-posted — and it is how a fan-out starts summarising its
|
|
815
|
+
own summaries. Every comment these verbs post carries one on the way out; you
|
|
816
|
+
never write one in.
|
|
817
|
+
|
|
818
|
+
`say` starts from the **issue** end only. A work item may hyperlink several
|
|
819
|
+
issues, so a bare id names no single reporter — `pharos issue trail <id>` is how
|
|
820
|
+
you find out which one you meant.
|
|
821
|
+
|
|
822
|
+
### `trail`, and the evidence worth reading
|
|
823
|
+
|
|
824
|
+
Read-only, and it answers from **either** end with the same output shape. The
|
|
825
|
+
field to read is `evidence`: `["hyperlink","marker"]` is a complete link, and
|
|
826
|
+
either one **alone** is a link that only half exists — a finding, not a detail.
|
|
827
|
+
The halves live in different places on purpose: a `Hyperlink` relation on the
|
|
828
|
+
work item, and one comment on the issue carrying `AB#4821` plus the
|
|
829
|
+
machine-readable trailer.
|
|
830
|
+
|
|
831
|
+
**The marker goes in a comment, never the issue body.** Editing a reporter's
|
|
832
|
+
body collides with them and needs write access nobody has on a community issue.
|
|
833
|
+
|
|
834
|
+
### `drift` — the report no other tool can produce
|
|
835
|
+
|
|
836
|
+
`gh` lists issues and Azure DevOps lists work items; **neither holds both
|
|
837
|
+
sides**, so neither can say *these two disagree*. Four kinds, because they want
|
|
838
|
+
four different actions:
|
|
839
|
+
|
|
840
|
+
| kind | what it means |
|
|
841
|
+
|---|---|
|
|
842
|
+
| `state` | both ends exist and disagree about whether the work is finished |
|
|
843
|
+
| `missing-issue` | the work item names an issue GitHub does not have |
|
|
844
|
+
| `one-sided` | one platform carries the link and the other does not — what a failed `adopt` leaves. Carries the `pharos issue link …` that repairs it |
|
|
845
|
+
| `unreadable` | the GitHub end could not be read, so nothing about this pair is known |
|
|
846
|
+
|
|
847
|
+
**It scans links, not issues.** An unadopted issue is not drift — that is
|
|
848
|
+
`backfill`'s question. On a repo with four hundred of them, counting "not
|
|
849
|
+
linked" as a problem buries three real findings under three hundred and
|
|
850
|
+
eighty-eight rows of noise.
|
|
851
|
+
|
|
852
|
+
**"Finished" is read from the project, never guessed**: each work item is judged
|
|
853
|
+
against its own type's categories and the report says which source it used —
|
|
854
|
+
`Resolved` is terminal on a Bug and open on a User Story. When the catalogue
|
|
855
|
+
cannot be read the stock names are used *and said out loud*, because a row is
|
|
856
|
+
uninterpretable without knowing what was counted as finished. An unreadable
|
|
857
|
+
repository is information rather than a fault and never fails the command.
|
|
858
|
+
|
|
859
|
+
### `backfill` — the adoption path for the repo that already has 400 issues
|
|
860
|
+
|
|
861
|
+
```bash
|
|
862
|
+
pharos issue backfill contoso/widgets # previews, then REFUSES (exit 3)
|
|
863
|
+
pharos issue backfill contoso/widgets --dry-run # the same preview, exit 0
|
|
864
|
+
pharos issue backfill contoso/widgets --label bug --parent 39 --limit 25 --max-writes 50 --yes
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
**Read the preview before adding `--yes`.** It names the repository, the `gh`
|
|
868
|
+
account it would be reached as, **and the Azure DevOps organisation and project
|
|
869
|
+
the work items would land on** — nothing in the design pairs a repo with a
|
|
870
|
+
board, and a mis-aimed `adopt` is one work item where a mis-aimed `backfill
|
|
871
|
+
--yes` is four hundred, each with a public comment naming a board its reporter
|
|
872
|
+
has nothing to do with.
|
|
873
|
+
|
|
874
|
+
**Each adoption is two writes**, so four hundred issues is eight hundred against
|
|
875
|
+
a default cap of 20. That cost is worked out *before* anything is written and a
|
|
876
|
+
run that cannot finish is refused, naming both ways forward — rather than
|
|
877
|
+
stopping at write nineteen, which is the outcome you can reason about least.
|
|
878
|
+
|
|
879
|
+
**It is resumable and needs no state file.** The link *is* the progress record:
|
|
880
|
+
a second run finds the ones the first adopted already linked and carries on, in
|
|
881
|
+
issue-number order. `--state` defaults to `open`, because adopting a closed
|
|
882
|
+
issue creates a work item that is finished before anybody looks at it.
|
|
883
|
+
|
|
884
|
+
### `close` — the ending of the trail
|
|
885
|
+
|
|
886
|
+
Closes **both** ends, comments on each pointing at the other, then **re-reads
|
|
887
|
+
both** and reports what actually moved. Two 200s are not proof: a workflow rule
|
|
888
|
+
can refuse a transition on a field the API happily accepted, so **`verified` is
|
|
889
|
+
what was there afterwards** rather than what was sent. Read it.
|
|
890
|
+
|
|
891
|
+
**There is no hardcodable `Closed`.** The state comes from the project's own
|
|
892
|
+
categories — exactly one terminal state for that type is used, and *several* is
|
|
893
|
+
a refusal naming them and `--to`, because `Done` and `Removed` are both finished
|
|
894
|
+
and mean opposite things. If the catalogue cannot be read at all it refuses
|
|
895
|
+
rather than guessing.
|
|
896
|
+
|
|
897
|
+
`--to`, **not** `--state`: `--state` already means `open|closed|all` on
|
|
898
|
+
`backfill`. Closing an already-closed pair writes nothing and exits 0. A work
|
|
899
|
+
item linking several issues is refused unless `--all` says so — "close 4821"
|
|
900
|
+
should not read as "close four strangers' issues".
|
|
901
|
+
|
|
902
|
+
**Pull requests are out of scope and are refused by name.** On GitHub's API
|
|
903
|
+
every PR is also an issue, so `adopt` on a PR number would otherwise mirror
|
|
904
|
+
something none of this models. `close` does not compose `Closes #45` /
|
|
905
|
+
`Fixes AB#123` into a PR body either: those fire on a PR *merge*, so there would
|
|
906
|
+
be nothing to compose into and nothing to verify.
|
|
907
|
+
|
|
908
|
+
### Editing a GitHub comment, and the trailer you must not break
|
|
909
|
+
|
|
910
|
+
A comment that already exists is `pharos comment`, with the issue as the target.
|
|
911
|
+
The verb set is the one you already know from work items and wiki pages:
|
|
912
|
+
|
|
913
|
+
```bash
|
|
914
|
+
pharos comment list contoso/widgets#45 # ids, state, and what you MAY do
|
|
915
|
+
pharos comment edit contoso/widgets#45 900 --file fixed.md
|
|
916
|
+
pharos comment delete contoso/widgets#45 900 --yes
|
|
917
|
+
pharos comment react contoso/widgets#45 900 thumbs-up
|
|
918
|
+
pharos comment hide contoso/widgets#45 900 off-topic
|
|
919
|
+
pharos comment pin contoso/widgets#45 900
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
**Start with `list`.** It is the only way to learn a comment id, and it also
|
|
923
|
+
answers the question that saves a failed write: every row carries a `may` object
|
|
924
|
+
— `edit`, `delete`, `hide`, `pin`, `react` — read from GitHub's own view of what
|
|
925
|
+
this account may do. A comment you may not edit is `"edit": false` before you
|
|
926
|
+
try, not a 403 afterwards. Each row also carries `pharosAuthored` and
|
|
927
|
+
`workItemId`, so you can see at a glance which comment is the link.
|
|
928
|
+
|
|
929
|
+
Two ids come back per comment and **both are printed because neither can be
|
|
930
|
+
derived from the other**: `id` is the number that edit, delete and reactions
|
|
931
|
+
address, `nodeId` is what hide and pin take. Either is accepted anywhere a
|
|
932
|
+
comment is named; passing back the one the verb wants saves a request.
|
|
933
|
+
|
|
934
|
+
**An edit cannot double or strip the `pharos:v1` trailer, and this is enforced
|
|
935
|
+
rather than requested.** That trailer is the durable record of which work item
|
|
936
|
+
tracks this issue *and* the guard that stops the two platforms summarising each
|
|
937
|
+
other's summaries. So:
|
|
938
|
+
|
|
939
|
+
| what you send | what happens |
|
|
940
|
+
|---|---|
|
|
941
|
+
| new prose, no trailer | the existing trailer is **restored**, and `markerPreserved: true` says so |
|
|
942
|
+
| the body you read back, trailer intact | sent unchanged |
|
|
943
|
+
| two trailers | refused — `doubled` |
|
|
944
|
+
| a trailer naming a different work item | refused — `repointed`; `pharos issue link` is the verb for that |
|
|
945
|
+
| a trailer on a comment that had none | refused — `forged` |
|
|
946
|
+
|
|
947
|
+
You do not have to think about any of this: hand over the words you want and the
|
|
948
|
+
record survives. It is written down because the refusals name a reason, and the
|
|
949
|
+
reason is actionable.
|
|
950
|
+
|
|
951
|
+
**Deleting the comment that carries the trailer destroys the GitHub half of the
|
|
952
|
+
link.** It is allowed — a mis-adoption is a real thing — but the `--yes` refusal
|
|
953
|
+
says so first, names the work item, and gives you the `pharos issue link` command
|
|
954
|
+
that puts it back. Under `--dry-run` you get the same preview and exit 0. A
|
|
955
|
+
delete that finds the comment already gone reports `alreadyGone: true` and
|
|
956
|
+
succeeds, so a retry after a lost reply is safe.
|
|
957
|
+
|
|
958
|
+
**`hide` and `pin` exist on GitHub and nowhere else** — they are GraphQL
|
|
959
|
+
mutations with no REST route, which is why `gh` has no verb for them. Hiding
|
|
960
|
+
needs a classifier and there is no neutral default, because GitHub shows it
|
|
961
|
+
beside the hidden comment: `spam`, `abuse`, `off-topic`, `outdated`,
|
|
962
|
+
`duplicate`, `resolved`, `low-quality`. Ask for `off-topic` and `off-topic` is
|
|
963
|
+
what reads back. On a work item or a wiki page these four verbs are refused with
|
|
964
|
+
"Azure DevOps has neither" rather than as an unknown verb — there is no endpoint
|
|
965
|
+
to go looking for.
|
|
966
|
+
|
|
967
|
+
**`pharos comment add` is refused on a GitHub issue.** Posting is already
|
|
968
|
+
covered twice and which one you want depends on whether the board should hear:
|
|
969
|
+
`pharos issue say` writes to the reporter *and* the work item, `gh issue comment`
|
|
970
|
+
writes to GitHub only. A third door here would post on an adopted issue that the
|
|
971
|
+
work item never hears about — the exact asymmetry `say` exists to prevent.
|
|
972
|
+
|
|
599
973
|
## What `pharos` does NOT do — read this before you go looking
|
|
600
974
|
|
|
601
975
|
- **Free-text and code search.** Nothing here covers it. For work items,
|
|
@@ -606,6 +980,14 @@ leading slash**. Two things follow:
|
|
|
606
980
|
- **Wiki content search.** `wiki tree` then `wiki read` is the only way through;
|
|
607
981
|
there is no grep across pages.
|
|
608
982
|
- Pull requests, builds, pipelines.
|
|
983
|
+
- **Almost anything on GitHub that is not the join.** No issue list, no issue
|
|
984
|
+
view, no *posting* a comment, no labels or milestones, no pull requests — `gh`
|
|
985
|
+
does all of it better and a second GitHub CLI would only drift from it. The
|
|
986
|
+
seven `issue` verbs are the edge. The one addition is a comment that already
|
|
987
|
+
exists (`pharos comment <verb> <owner/name#45>`): editing or deleting one can
|
|
988
|
+
break the link record, and hiding or pinning has no `gh` verb at all.
|
|
989
|
+
- **Jira, Linear, and the Argus board.** Not covered, not planned. "Task",
|
|
990
|
+
"todo" and "board" mean something else there.
|
|
609
991
|
|
|
610
992
|
**There is no Azure DevOps MCP server here any more, and that is deliberate.**
|
|
611
993
|
It authenticated through the Azure CLI, so it opened a browser mid-task — which
|
|
@@ -617,9 +999,9 @@ routing around it: several things here exist in no other Azure DevOps tool at
|
|
|
617
999
|
all — wiki page comments and reactions, attachment upload AND removal, inline
|
|
618
1000
|
images for a work item field or a wiki page, mentioning somebody in a form that
|
|
619
1001
|
actually notifies, importing a Word document or a PDF as a page, editing or
|
|
620
|
-
deleting a work item comment, service hooks,
|
|
621
|
-
`test` op on `/rev` so a
|
|
622
|
-
overwritten. Those are what the
|
|
1002
|
+
deleting a work item comment, service hooks, the GitHub-issue join and the
|
|
1003
|
+
drift report over it, and applying a change under a `test` op on `/rev` so a
|
|
1004
|
+
teammate who wrote first cannot be silently overwritten. Those are what the
|
|
623
1005
|
guards are, and they are the reason to come back here rather than hand-roll.
|
|
624
1006
|
|
|
625
1007
|
## Start every task with one command
|
|
@@ -739,7 +1121,12 @@ true` means stop re-checking the token.
|
|
|
739
1121
|
| `0` | it worked | carry on |
|
|
740
1122
|
| `1` | the call failed | check `kind`; retry only if it is `rateLimit` (wait `retryAfterMs`) or transient |
|
|
741
1123
|
| `2` | called wrong, or not configured | **never retry unchanged.** Fix the call, or the setup |
|
|
742
|
-
| `3` | a guard here refused | re-run with `--yes` — after deciding it is right |
|
|
1124
|
+
| `3` | a guard here refused | re-run with `--yes`, or raise `--max-writes` — after deciding it is right |
|
|
1125
|
+
|
|
1126
|
+
**A failure carrying `"platform": "github"` came from GitHub**, not from Azure
|
|
1127
|
+
DevOps and not from here — `pharos issue` touches two platforms in one call, and
|
|
1128
|
+
which one refused decides whether the fix is a token, a repo binding or a field.
|
|
1129
|
+
Its absence means the other side, as it always did.
|
|
743
1130
|
|
|
744
1131
|
`"kind": "conflict"` means somebody wrote first. Your work is still valid:
|
|
745
1132
|
re-read, re-apply. It carries both revisions — and note that **posting a comment
|
|
@@ -801,6 +1188,12 @@ it is about to break. Sub-pages move with their parent.
|
|
|
801
1188
|
not only in a chat log nobody else can read.
|
|
802
1189
|
5. Move the state when the work moves, not at the end.
|
|
803
1190
|
|
|
1191
|
+
**When the work started as a GitHub issue the loop gains two ends and changes
|
|
1192
|
+
one step**: `pharos issue adopt` before step 1, `pharos issue close` after step
|
|
1193
|
+
5, and at step 4 the reply to the *reporter* is `pharos issue say` rather than a
|
|
1194
|
+
comment on the work item — a comment on the item is invisible to somebody with
|
|
1195
|
+
no Azure DevOps account, which is every reporter.
|
|
1196
|
+
|
|
804
1197
|
**Every person uses their own token.** Board attribution is per-person, so
|
|
805
1198
|
anything you do is recorded against whoever owns `ADO_PAT`. Never suggest
|
|
806
1199
|
sharing one.
|
|
@@ -813,3 +1206,21 @@ profile that **scripted** shells read, and verifies both permission scopes —
|
|
|
813
1206
|
Work Items and Wiki are separate in Azure DevOps, and a token missing the second
|
|
814
1207
|
works fine until the first wiki write days later. A **new** shell is needed
|
|
815
1208
|
afterwards.
|
|
1209
|
+
|
|
1210
|
+
**The GitHub half is configured separately and follows a different rule.** The
|
|
1211
|
+
environment carries no fourth secret: the credential comes from `gh`, and which
|
|
1212
|
+
account reaches which repository is a binding on disk, so a `config` error from
|
|
1213
|
+
an `issue` verb names `repos.json` rather than an environment variable.
|
|
1214
|
+
|
|
1215
|
+
```bash
|
|
1216
|
+
pharos setup --repo contoso/widgets --gh-account alisina-tibata
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
**A verb whose target is a GitHub issue does not need `ADO_PAT` at all.**
|
|
1220
|
+
`pharos comment list|edit|delete|react|unreact|reactors|hide|unhide|pin|unpin
|
|
1221
|
+
<owner/name#n>` runs on `gh`'s credential alone, on a machine that has never had
|
|
1222
|
+
an Azure DevOps token. So a `config` error naming `ADO_PAT` from one of those is
|
|
1223
|
+
a defect worth reporting, not something to go and provision a token over. The
|
|
1224
|
+
two credentials are independent — separate keychain entries, and `pharos doctor`
|
|
1225
|
+
reports them separately — and `pharos issue` is the one family that genuinely
|
|
1226
|
+
needs both, because it writes to both platforms in one invocation.
|