@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.
Files changed (49) hide show
  1. package/README.md +409 -5
  2. package/dist/capabilities.d.ts.map +1 -1
  3. package/dist/capabilities.js +23 -0
  4. package/dist/capabilities.js.map +1 -1
  5. package/dist/cli.d.ts +4 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +362 -36
  8. package/dist/cli.js.map +1 -1
  9. package/dist/commands/comment-github.d.ts +138 -0
  10. package/dist/commands/comment-github.d.ts.map +1 -0
  11. package/dist/commands/comment-github.js +427 -0
  12. package/dist/commands/comment-github.js.map +1 -0
  13. package/dist/commands/comment.d.ts.map +1 -1
  14. package/dist/commands/comment.js +114 -19
  15. package/dist/commands/comment.js.map +1 -1
  16. package/dist/commands/doctor.d.ts +71 -2
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +414 -8
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/issue.d.ts +75 -0
  21. package/dist/commands/issue.d.ts.map +1 -0
  22. package/dist/commands/issue.js +2466 -0
  23. package/dist/commands/issue.js.map +1 -0
  24. package/dist/commands/setup.d.ts +6 -0
  25. package/dist/commands/setup.d.ts.map +1 -1
  26. package/dist/commands/setup.js +114 -0
  27. package/dist/commands/setup.js.map +1 -1
  28. package/dist/half-link.d.ts +157 -0
  29. package/dist/half-link.d.ts.map +1 -0
  30. package/dist/half-link.js +257 -0
  31. package/dist/half-link.js.map +1 -0
  32. package/dist/issue-scan.d.ts +383 -0
  33. package/dist/issue-scan.d.ts.map +1 -0
  34. package/dist/issue-scan.js +455 -0
  35. package/dist/issue-scan.js.map +1 -0
  36. package/dist/output.d.ts +18 -1
  37. package/dist/output.d.ts.map +1 -1
  38. package/dist/output.js +293 -0
  39. package/dist/output.js.map +1 -1
  40. package/dist/session.d.ts +101 -2
  41. package/dist/session.d.ts.map +1 -1
  42. package/dist/session.js +132 -11
  43. package/dist/session.js.map +1 -1
  44. package/dist/target.d.ts +40 -4
  45. package/dist/target.d.ts.map +1 -1
  46. package/dist/target.js +14 -3
  47. package/dist/target.js.map +1 -1
  48. package/package.json +3 -2
  49. 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 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."
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 OR a wiki path
63
- comment react | unreact | reactors like dislike heart hooray smile confused
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, offline, and works before anything is configured — so it is also the
87
- right first move when something behaves oddly.
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, and applying a change under a
621
- `test` op on `/rev` so a teammate who wrote first cannot be silently
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.