@celsoft/coordinator 0.0.0 → 0.1.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 (131) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +107 -0
  3. package/THIRD_PARTY_NOTICES.md +2005 -0
  4. package/agents/console.md +20 -0
  5. package/agents/free.md +110 -0
  6. package/build/0-JP6XL6LB.js +17 -0
  7. package/build/1-A52YZWNS.js +17 -0
  8. package/build/2-CLH2NM6F.js +32 -0
  9. package/build/3-4N467IB6.js +45 -0
  10. package/build/4-WL5H4LUW.js +29 -0
  11. package/build/_layout.svelte-KPUSX7LQ.js +27 -0
  12. package/build/_page.svelte-22H56UR6.js +7972 -0
  13. package/build/_page.svelte-CVWUNRQC.js +135 -0
  14. package/build/_page.svelte-LTZXBHMT.js +57 -0
  15. package/build/_server.ts-3KVBYN6P.js +20 -0
  16. package/build/_server.ts-3POPKVEM.js +47 -0
  17. package/build/_server.ts-5735T2V5.js +31 -0
  18. package/build/_server.ts-5VXGNWV2.js +38 -0
  19. package/build/_server.ts-645KP7WP.js +42 -0
  20. package/build/_server.ts-A3AA24Z2.js +28 -0
  21. package/build/_server.ts-CITUOEM6.js +58 -0
  22. package/build/_server.ts-I7HMTPOA.js +25 -0
  23. package/build/_server.ts-M5BQ4LLX.js +30 -0
  24. package/build/_server.ts-OB4O27HF.js +683 -0
  25. package/build/_server.ts-OJT7HE7M.js +15 -0
  26. package/build/_server.ts-OYGWVLZM.js +12 -0
  27. package/build/_server.ts-T75UVMQP.js +38 -0
  28. package/build/_server.ts-WYZYNHUZ.js +28 -0
  29. package/build/_server.ts-Z26EB47B.js +486 -0
  30. package/build/chunk-4CIG53L5.js +33 -0
  31. package/build/chunk-BJTJS3XY.js +338 -0
  32. package/build/chunk-F2CNWJZM.js +14 -0
  33. package/build/chunk-HGYE3UTU.js +416 -0
  34. package/build/chunk-KAYW2W7H.js +1311 -0
  35. package/build/chunk-LFP3UPCM.js +10 -0
  36. package/build/chunk-M44JEFEH.js +62 -0
  37. package/build/chunk-MC5AGLMY.js +39 -0
  38. package/build/chunk-NCZ77J5E.js +19798 -0
  39. package/build/chunk-NVNLDPUJ.js +2122 -0
  40. package/build/chunk-PDCON4KS.js +494 -0
  41. package/build/chunk-Q4OQNY5M.js +25 -0
  42. package/build/chunk-QCUOR2U5.js +75 -0
  43. package/build/chunk-R6RYZZWA.js +46 -0
  44. package/build/chunk-SOJTASZC.js +51 -0
  45. package/build/chunk-SU26PJVX.js +410 -0
  46. package/build/chunk-U5BSBG4R.js +506 -0
  47. package/build/chunk-UEKOZGRO.js +37 -0
  48. package/build/chunk-W6EFUWJ2.js +9 -0
  49. package/build/chunk-WOPREK6Q.js +117 -0
  50. package/build/chunk-WWJ3F4QS.js +127 -0
  51. package/build/chunk-ZGZJEEE3.js +1091 -0
  52. package/build/client/_app/immutable/assets/0.BGa5ZNDM.css +1 -0
  53. package/build/client/_app/immutable/chunks/BUrrLYDt.js +2 -0
  54. package/build/client/_app/immutable/chunks/BgFr_EyZ.js +4 -0
  55. package/build/client/_app/immutable/chunks/Bjy-W4x2.js +82 -0
  56. package/build/client/_app/immutable/chunks/C8qZab0y.js +56 -0
  57. package/build/client/_app/immutable/chunks/C90fLsA5.js +4 -0
  58. package/build/client/_app/immutable/chunks/CRzLMJPS.js +1 -0
  59. package/build/client/_app/immutable/chunks/DLT1iIlz.js +2 -0
  60. package/build/client/_app/immutable/chunks/pIrVVOVY.js +2 -0
  61. package/build/client/_app/immutable/entry/app.0DERehLW.js +3 -0
  62. package/build/client/_app/immutable/entry/payload.DSmR2FwN.js +2 -0
  63. package/build/client/_app/immutable/entry/start.B7srFiqh.js +3 -0
  64. package/build/client/_app/immutable/nodes/0.aD9vJoxR.js +2 -0
  65. package/build/client/_app/immutable/nodes/1.Br4FL91i.js +2 -0
  66. package/build/client/_app/immutable/nodes/2.vl6OYfu3.js +8 -0
  67. package/build/client/_app/immutable/nodes/3.VYqJOLAF.js +2 -0
  68. package/build/client/_app/immutable/nodes/4.jF9UVlFN.js +2 -0
  69. package/build/client/_app/version.json +1 -0
  70. package/build/client/apple-touch-icon.png +0 -0
  71. package/build/client/favicon.ico +0 -0
  72. package/build/client/favicon.svg +48 -0
  73. package/build/client/icon-192.png +0 -0
  74. package/build/client/icon-512.png +0 -0
  75. package/build/client/icon-maskable-512.png +0 -0
  76. package/build/client/icon.svg +44 -0
  77. package/build/client/manifest.webmanifest +15 -0
  78. package/build/error.svelte-JXZDY65F.js +51 -0
  79. package/build/handler.js +716 -0
  80. package/build/hooks.server-34IMHRKC.js +43 -0
  81. package/build/instance-NP3FJIWW.js +2967 -0
  82. package/client/client.mjs +28 -0
  83. package/client/hooks.mjs +7 -0
  84. package/client/register.mjs +5 -0
  85. package/dist/cli.js +62826 -0
  86. package/dist/port.js +36691 -0
  87. package/package.json +20 -1
  88. package/skills/agents/.claude-plugin/plugin.json +5 -0
  89. package/skills/agents/hooks/hooks.json +1 -0
  90. package/skills/agents/hooks/register.ts +57 -0
  91. package/skills/agents/skills/bus-work/SKILL.md +66 -0
  92. package/skills/agents/skills/plan/SKILL.md +98 -0
  93. package/skills/console/.claude-plugin/plugin.json +5 -0
  94. package/skills/console/hooks/hooks.json +1 -0
  95. package/skills/console/hooks/register.ts +10 -0
  96. package/skills/console/skills/bus/SKILL.md +61 -0
  97. package/skills/default/.claude-plugin/plugin.json +5 -0
  98. package/skills/default/skills/git-workflow/SKILL.md +294 -0
  99. package/skills/default/skills/git-workflow/mise-tasks/_lib.sh +107 -0
  100. package/skills/default/skills/git-workflow/mise-tasks/clone +36 -0
  101. package/skills/default/skills/git-workflow/mise-tasks/repo-list +8 -0
  102. package/skills/default/skills/git-workflow/mise-tasks/unpushed +26 -0
  103. package/skills/default/skills/git-workflow/mise-tasks/worktree-add +60 -0
  104. package/skills/default/skills/github/SKILL.md +208 -0
  105. package/skills/default/skills/github/scripts/gh-org +196 -0
  106. package/skills/default/skills/plan-page/SKILL.md +300 -0
  107. package/skills/default/skills/plan-page/assets/index.html +854 -0
  108. package/skills/default/skills/plan-page/scripts/inline +46 -0
  109. package/skills/default/skills/plan-page/scripts/stage +26 -0
  110. package/skills/default/skills/secrets/SKILL.md +134 -0
  111. package/skills/default/skills/secrets/scripts/secrets-check +87 -0
  112. package/skills/default/skills/writing-style/SKILL.md +45 -0
  113. package/skills/extra/.claude-plugin/plugin.json +5 -0
  114. package/skills/extra/skills/artifacts/SKILL.md +52 -0
  115. package/skills/extra/skills/github-actions/SKILL.md +78 -0
  116. package/skills/extra/skills/github-actions/scan +32 -0
  117. package/skills/extra/skills/markdown-review/SKILL.md +70 -0
  118. package/skills/extra/skills/markdown-review/assets/index.html +201 -0
  119. package/skills/extra/skills/markdown-review/scripts/stage +36 -0
  120. package/skills/extra/skills/mise-usage/SKILL.md +155 -0
  121. package/skills/extra/skills/plugin-authoring/SKILL.md +317 -0
  122. package/skills/extra/skills/project-conventions/SKILL.md +64 -0
  123. package/skills/extra/skills/questions/SKILL.md +156 -0
  124. package/skills/extra/skills/questions/assets/index.html +376 -0
  125. package/skills/extra/skills/questions/scripts/stage +26 -0
  126. package/skills/extra/skills/serve/SKILL.md +165 -0
  127. package/skills/extra/skills/serve/scripts/serve +268 -0
  128. package/skills/extra/skills/serve/scripts/server.py +345 -0
  129. package/skills/extra/skills/skill-dependencies/SKILL.md +65 -0
  130. package/skills/extra/skills/skill-maintenance/SKILL.md +101 -0
  131. package/skills/extra/skills/ticketing/SKILL.md +125 -0
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env zsh
2
+ # Shared helpers for the layout tasks. Not a task itself (leading underscore).
3
+ # DEV_ROOT and the two path helpers below are the sole definition of the repo/worktree
4
+ # layout. SKILL.md documents it; every other skill defers to it rather than hardcoding.
5
+
6
+ [[ -n "${MISE_ORIGINAL_CWD}" ]] && cd "$MISE_ORIGINAL_CWD"
7
+
8
+ header() { print -P "\n%B$*%b" >&2; }
9
+ success() { print -P "%F{green}✓%f $*" >&2; }
10
+ info() { print -P "%F{cyan}•%f $*" >&2; }
11
+ warn() { print -P "%F{yellow}!%f $*" >&2; }
12
+ error() { print -P "%F{red}✗%f $*" >&2; }
13
+ dim() { print -P "%F{8}$*%f" >&2; }
14
+ # Every task's only stdout is its final resolved path, printed with `path`, so a
15
+ # caller captures it with $(...) instead of re-deriving repo_path/worktree_path
16
+ # itself. Human-readable progress above goes to stderr and is unaffected.
17
+ path() { print "$*"; }
18
+
19
+ DEV_ROOT="${DEVELOPMENT_ROOT:-$HOME/Development}"
20
+
21
+ repo_path() { print "$DEV_ROOT/repos/$1/$2"; }
22
+ worktree_path() { print "$DEV_ROOT/worktrees/$3/$1/$2"; }
23
+
24
+ # Branches may contain slashes (e.g. env/sean/RKG-7734), which nest as real
25
+ # directories. That makes the branch/org boundary unrecoverable from the path,
26
+ # so anything reading a worktree back asks git rather than parsing.
27
+ branch_of() { git -C "$1" branch --show-current; }
28
+
29
+ # A caller hands over whatever checkout it happens to be sitting in, which may be
30
+ # a linked worktree. Its refs, remotes and worktree list are the main checkout's,
31
+ # so resolve to that one and treat the two identically.
32
+ main_checkout() {
33
+ local common
34
+ common=$(git -C "$1" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 1
35
+ print "${common:h}"
36
+ }
37
+
38
+ # org/repo comes from origin's URL, never from where the checkout sits on disk:
39
+ # that is what lets a clone outside repos/ (a tool's own source directory, say)
40
+ # still land its worktrees under its own org/repo with the rest of the set.
41
+ # Handles ssh aliases (git@github.com-acme:org/repo.git) and https alike.
42
+ repo_slug() {
43
+ local url remote_path org repo
44
+ url=$(git -C "$1" config --get remote.origin.url 2>/dev/null)
45
+ if [[ -n "$url" ]]; then
46
+ remote_path=${url%.git}
47
+ remote_path=${remote_path#*://}
48
+ remote_path=${remote_path#*@}
49
+ remote_path=${remote_path/:/\/}
50
+ else
51
+ # No remote to ask: the checkout and its parent stand in for repo and org.
52
+ remote_path=${1:A}
53
+ fi
54
+ repo=${remote_path:t}
55
+ org=${${remote_path:h}:t}
56
+ if [[ -z "$org" || -z "$repo" ]]; then
57
+ error "Cannot determine org/repo for $1"
58
+ dim " Set a remote: git -C $1 remote add origin git@github.com:<org>/<repo>.git"
59
+ return 1
60
+ fi
61
+ print "$org/$repo"
62
+ }
63
+
64
+ need_git() {
65
+ if ! command -v git &>/dev/null; then
66
+ error "'git' is not on PATH."
67
+ exit 1
68
+ fi
69
+ }
70
+
71
+ require_checkout() {
72
+ if ! git -C "$1" rev-parse --git-dir &>/dev/null; then
73
+ error "Not a git checkout: $1"
74
+ exit 1
75
+ fi
76
+ }
77
+
78
+ # origin/HEAD is only populated by a full clone or an explicit set-head, so ask the
79
+ # remote before giving up. Never guess a name: branching a new worktree off the wrong
80
+ # base looks like it worked, so an undeterminable default is a hard error the caller
81
+ # resolves with the user.
82
+ # An explicit base. Prefer the remote ref: a stale local branch of the same name is the
83
+ # likelier mistake. Never invent one: an unresolvable base is a hard error, because
84
+ # branching off the wrong commit reports success just as loudly as branching off the right
85
+ # one.
86
+ resolve_base() {
87
+ local dir="$1" base="$2"
88
+ git -C "$dir" show-ref --verify --quiet "refs/remotes/origin/$base" \
89
+ && { print "origin/$base"; return }
90
+ git -C "$dir" show-ref --verify --quiet "refs/heads/$base" \
91
+ && { print "$base"; return }
92
+ error "Base branch '$base' does not exist locally or on origin in $dir"
93
+ dim " Check the name, or fetch first: git -C $dir fetch origin"
94
+ return 1
95
+ }
96
+
97
+ default_branch() {
98
+ local dir="$1" ref
99
+ ref=$(git -C "$dir" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null) \
100
+ && { print "${ref#refs/remotes/origin/}"; return }
101
+ git -C "$dir" remote set-head origin --auto >/dev/null 2>&1 \
102
+ && ref=$(git -C "$dir" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null) \
103
+ && { print "${ref#refs/remotes/origin/}"; return }
104
+ error "Cannot determine the default branch of origin in $dir"
105
+ dim " Set it explicitly: git -C $dir remote set-head origin <branch>"
106
+ return 1
107
+ }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env zsh
2
+ #MISE description="Clone <org>/<repo> into $DEVELOPMENT_ROOT/repos/<org>/<repo> (idempotent)"
3
+ #USAGE arg "<org>" help="GitHub org or user"
4
+ #USAGE arg "<repo>" help="Repository name"
5
+
6
+ source "${0:A:h}/_lib.sh"
7
+
8
+ org="${1:?usage: clone <org> <repo>}"
9
+ repo="${2:?usage: clone <org> <repo>}"
10
+
11
+ need_git
12
+
13
+ dest=$(repo_path "$org" "$repo")
14
+
15
+ if [[ -d "$dest/.git" ]]; then
16
+ success "Already cloned: $dest"
17
+ path "$dest"
18
+ exit 0
19
+ fi
20
+
21
+ if [[ -e "$dest" ]]; then
22
+ error "$dest exists but is not a git clone."
23
+ exit 1
24
+ fi
25
+
26
+ # SSH so any per-org keys and URL rewrites apply.
27
+ info "Cloning $org/$repo into $dest..."
28
+ mkdir -p "${dest:h}"
29
+ if ! git clone "git@github.com:$org/$repo.git" "$dest"; then
30
+ error "Clone failed for $org/$repo."
31
+ dim " If this is an auth or permission error, the SSH key in use is not one GitHub accepts for '$org'."
32
+ exit 1
33
+ fi
34
+
35
+ success "Cloned to $dest"
36
+ path "$dest"
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env zsh
2
+ #MISE description="List every clone under $DEVELOPMENT_ROOT/repos as org/repo"
3
+
4
+ source "${0:A:h}/_lib.sh"
5
+
6
+ for dir in "$DEV_ROOT"/repos/*/*(N/); do
7
+ [[ -d "$dir/.git" ]] && print "${${dir%/}#$DEV_ROOT/repos/}"
8
+ done
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env zsh
2
+ #MISE description="Print every commit a push would send, oldest first, with full messages"
3
+
4
+ source "${0:A:h}/_lib.sh"
5
+
6
+ need_git
7
+
8
+ default=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null)
9
+ git fetch --quiet origin 2>/dev/null || warn "Fetch failed; comparing against the last known remote state."
10
+
11
+ # A linked worktree pushes by fast-forwarding the default branch, so that is the target.
12
+ # A clone pushes its own branch: its upstream if set, else the default branch.
13
+ if [[ "$(git rev-parse --git-dir)" != "$(git rev-parse --git-common-dir)" ]]; then
14
+ base=$default
15
+ else
16
+ base=$(git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null) || base=$default
17
+ fi
18
+
19
+ if [[ -z "$base" ]]; then
20
+ error "No upstream and no origin/HEAD to compare against."
21
+ exit 1
22
+ fi
23
+
24
+ count=$(git rev-list --count "$base..HEAD")
25
+ info "$count unpushed commit(s) on $(git branch --show-current), compared with $base"
26
+ git log --reverse --format='%h %s%n%n%b' "$base..HEAD"
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env zsh
2
+ #MISE description="Add a worktree at $DEVELOPMENT_ROOT/worktrees/<branch>/<org>/<repo> (idempotent)"
3
+ #USAGE arg "<src>" help="Path to any checkout of the repo; a linked worktree is fine"
4
+ #USAGE arg "<branch>" help="Branch name; slashes nest as directories"
5
+ #USAGE arg "[base]" help="Branch to create from; defaults to the repo's default branch"
6
+
7
+ source "${0:A:h}/_lib.sh"
8
+
9
+ src="${usage_src:?usage: worktree-add <src> <branch> [base]}"
10
+ branch="${usage_branch:?usage: worktree-add <src> <branch> [base]}"
11
+ base="${usage_base:-}"
12
+
13
+ need_git
14
+ require_checkout "$src"
15
+
16
+ # The source is the only thing a caller has to know. Where the worktree goes
17
+ # follows from origin's org/repo, so a checkout anywhere on disk still lands its
18
+ # worktrees with the rest of that branch's set.
19
+ src=$(main_checkout "$src")
20
+ slug=$(repo_slug "$src") || exit 1
21
+ dest=$(worktree_path "${slug%/*}" "${slug#*/}" "$branch")
22
+
23
+ # git worktree add prints "HEAD is now at ..." on stdout, which would otherwise
24
+ # ride along with the path a caller captures. Every one of the three is >&2.
25
+ if [[ -d "$dest" ]]; then
26
+ success "Worktree already exists: $dest"
27
+ dim " On branch: $(branch_of "$dest")"
28
+ path "$dest"
29
+ exit 0
30
+ fi
31
+
32
+ git -C "$src" fetch origin --prune --quiet 2>/dev/null
33
+
34
+ mkdir -p "${dest:h}"
35
+
36
+ # The branch existing already settles where it came from, so a base is moot. Say so
37
+ # rather than ignoring it: a caller that passed one believes it is taking effect.
38
+ if git -C "$src" show-ref --verify --quiet "refs/heads/$branch"; then
39
+ [[ -n "$base" ]] && warn "Ignoring base '$base': local branch '$branch' already exists."
40
+ info "Checking out existing local branch '$branch'..."
41
+ git -C "$src" worktree add "$dest" "$branch" >&2
42
+ elif git -C "$src" show-ref --verify --quiet "refs/remotes/origin/$branch"; then
43
+ [[ -n "$base" ]] && warn "Ignoring base '$base': remote branch 'origin/$branch' already exists."
44
+ info "Tracking existing remote branch 'origin/$branch'..."
45
+ git -C "$src" worktree add --track -b "$branch" "$dest" "origin/$branch" >&2
46
+ else
47
+ if [[ -n "$base" ]]; then
48
+ start=$(resolve_base "$src" "$base") || exit 1
49
+ else
50
+ default=$(default_branch "$src") || exit 1
51
+ start="origin/$default"
52
+ fi
53
+ # --no-track: branching off a remote-tracking ref otherwise sets that ref as upstream
54
+ # (branch.autoSetupMerge), so a later bare `git push` would target the base branch.
55
+ info "Creating new branch '$branch' from $start..."
56
+ git -C "$src" worktree add --no-track -b "$branch" "$dest" "$start" >&2
57
+ fi
58
+
59
+ success "Worktree ready: $dest"
60
+ path "$dest"
@@ -0,0 +1,208 @@
1
+ ---
2
+ name: github
3
+ verified: 2026-09-03
4
+ description: "MUST READ before running any `gh` command that touches a repo, PR, or issue: several GitHub accounts are logged in at once, and this routes each command to the account that has access to the target org. Also covers stacked pull requests, verifying GitHub features against the live API rather than memory, installing `gh` via Homebrew when it is missing, and adding an account when auth fails."
5
+ ---
6
+
7
+ # GitHub CLI (`gh`), org-aware
8
+
9
+ The machine has **more than one GitHub account logged into `gh`** (a personal one, one per company org). Only one is "active", and it is whichever was switched to last: so a bare `gh pr create` regularly runs as the wrong identity and fails with a confusing *"Could not resolve to a Repository"*.
10
+
11
+ The fix is per-command, not global: this skill's `scripts/gh-org` picks the account for the **target org** and injects that account's token as `GH_TOKEN` for one invocation. Nothing global is switched, so parallel sessions on different orgs don't fight.
12
+
13
+ This wires the **API** side (`gh`). The **SSH/git** side (push, clone) is separate: an SSH key GitHub accepts for the org. Having one set up says nothing about the other. `git-workflow` owns commit rules, per-repo identity, and the on-disk repo/worktree layout.
14
+
15
+ ## The rule
16
+
17
+ Anything that touches a repo, PR, issue, run, or org goes through:
18
+
19
+ ```bash
20
+ <skill>/scripts/gh-org run [<org>] -- gh <args>...
21
+ ```
22
+
23
+ Omit `<org>` inside a git repo: it is parsed from the `origin` remote. Pass it explicitly when outside a repo or when targeting another org.
24
+
25
+ Bare `gh` is fine only for account-agnostic commands: `gh --version`, `gh auth status`, `gh auth login`.
26
+
27
+ **Never `gh repo clone`.** It writes into the current directory and ignores the on-disk layout. Cloning belongs to the `git-workflow` skill: read its **Repo layout** section and run its `clone` task, which places the repo correctly and uses SSH so the org keys apply.
28
+
29
+ Examples:
30
+
31
+ ```bash
32
+ scripts/gh-org run -- gh pr create --fill # org from the remote
33
+ scripts/gh-org run -- gh pr list --state open
34
+ scripts/gh-org run -- gh pr checks
35
+ scripts/gh-org run acme -- gh issue view 42 --repo acme/api --json title,body
36
+ scripts/gh-org run octo-user -- gh repo list --limit 20
37
+ ```
38
+
39
+ The script prints `gh-org: running as '<account>' for org '<org>'` to stderr: quote that account when reporting to the user, so they know which identity acted.
40
+
41
+ ## GitHub ships faster than your knowledge
42
+
43
+ Your training cutoff is older than GitHub's release cadence, so a feature you don't remember is more likely new than nonexistent. Assume that by default.
44
+
45
+ **When the user names a GitHub capability, verify before building anything.** Stacked PRs, merge queues, rulesets, sub-issues, Projects, auto-merge: check whether it exists as a first-party feature *before* hand-rolling an equivalent:
46
+
47
+ ```bash
48
+ gh <thing> --help # is there a built-in command?
49
+ gh extension list # is it an extension you don't have?
50
+ scripts/gh-org run -- gh api repos/<org>/<repo>/pulls/<n> | jq 'keys' # does the object carry its fields?
51
+ ```
52
+
53
+ Then check https://github.blog/changelog for anything newer than you remember. If the feature exists and you were about to build a manual substitute, say so and let the user choose: don't quietly ship the substitute.
54
+
55
+ **A user correcting you on a matter of fact is a signal to go verify, not to argue.** If they say "that's a feature now" and you believe otherwise, check before replying. Producing more evidence for your existing answer is the failure mode.
56
+
57
+ ## Never claim a first-party feature is in use without reading it back
58
+
59
+ Doing something that *resembles* a feature is not using it. Chaining base branches by hand is not a stack; a label named "merge queue" is not a merge queue.
60
+
61
+ Before telling the user a GitHub feature is active, query the object and show the field that proves it. If the field is absent, say the feature is not in use and name what you actually did instead. This applies to your own prior work in the session: re-check rather than trusting what you said earlier.
62
+
63
+ ## Stacked pull requests
64
+
65
+ First-party since 2026-07-30 (public preview). A stack is real GitHub state, not a base-branch convention.
66
+
67
+ **Identifying one:** a stacked PR carries a `stack` object in the REST payload (stack number and size, plus the PR's position and base); GraphQL exposes read-only `stack` and `stackEntry` fields on `PullRequest`. No `stack` key means it is not a stack, whatever the base branch says:
68
+
69
+ ```bash
70
+ scripts/gh-org run -- gh api repos/<org>/<repo>/pulls/<n> --jq 'has("stack")'
71
+ ```
72
+
73
+ **Requirements:** `gh` 2.90.0+, git 2.20+, and the extension:
74
+
75
+ ```bash
76
+ gh extension install github/gh-stack
77
+ ```
78
+
79
+ **Working with a stack:**
80
+
81
+ | Command | What it does |
82
+ |---|---|
83
+ | `gh stack init` | Creates the tracking entry and the first branch |
84
+ | `gh stack add <branch>` | Adds a branch to the stack |
85
+ | `gh stack add -Am "<message>"` | Stages, commits, and creates the next branch in one step |
86
+ | `gh stack push` | Pushes every branch in the stack |
87
+ | `gh stack submit` | Opens the PRs and links them as a stack on GitHub |
88
+ | `gh stack view` | Branches, PR links, statuses, and each branch's latest commit |
89
+
90
+ **Merging** is atomic across the stack (the whole group merges, or enters the merge queue, or none of it does) and requires the asynchronous merge endpoint rather than the legacy synchronous one.
91
+
92
+ **Rebasing** is handled for you: trigger a server-side cascading rebase from the PR, or run it locally through the extension. Don't hand-rebase each branch.
93
+
94
+ **Converting PRs that already exist needs no rebuild.** `gh stack link` takes PR numbers, PR URLs, or branch names in stack order (bottom to top) and links them into a real stack, keeping their numbers, review history, and draft state. Branches without a PR get one created with the right base chaining; PRs already in a stack are left alone.
95
+
96
+ ```bash
97
+ GH_REPO=<org>/<repo> scripts/gh-org run <org> -- gh stack link <bottom-pr> <top-pr>
98
+ ```
99
+
100
+ Two gotchas, both learned the hard way:
101
+
102
+ - **A custom SSH host alias breaks repo detection.** With a remote like `git@jll.github.com:org/repo.git`, `gh stack` fails with *"none of the git remotes ... point to a known GitHub host"*. There is no `--repo` flag; set `GH_REPO` instead, as above.
103
+ - **`--open` marks every PR in the stack ready for review**, drafts included. Omit it unless that is what you want.
104
+
105
+ `gh stack init <branch>...` likewise adopts existing branches rather than demanding new ones. So reach for `link` or `init` before ever proposing to rebuild a PR: the CLI's own `--help` is the authority here, and it documents more than the docs pages do.
106
+
107
+ ## How the account is resolved
108
+
109
+ 1. **The repo's own login:** inside a repo whose config sets `github.user` (a coordinator writes it, with the repo's identity and SSH key, into every repo it keeps), that login acts for the repo's own org, ahead of everything below.
110
+ 2. **Cached mapping:** `git config --global gh-org.account.<org>` (field first, org lowercased; git keys are case-insensitive, so `Acme` and `acme` are one entry).
111
+ 3. **Probe:** otherwise each logged-in account is tested: does its login equal `<org>` (personal namespace), or is it an active member of `<org>`? First match wins and is **cached** into the git config above.
112
+ 4. **Fail:** if no account has access, the script stops and says to run `gh auth login`.
113
+
114
+ Manage mappings directly when the probe guesses wrong:
115
+
116
+ ```bash
117
+ scripts/gh-org list # show org -> account mappings
118
+ scripts/gh-org map <org> <account> # pin one
119
+ scripts/gh-org unmap <org> # forget it (re-probes next time)
120
+ scripts/gh-org account [<org>] # print the account that would be used
121
+ ```
122
+
123
+ The `gh-org.*` namespace holds the per-account git identity too: see [Git identity setup](#git-identity-setup-one-time).
124
+
125
+ ## A pending invite looks like access
126
+
127
+ When an account should have org access but repo operations fail, check the **membership
128
+ state** rather than inferring it from a successful login. An unaccepted org invitation
129
+ authenticates fine and then denies every repo, which reads as a mysterious permissions bug:
130
+
131
+ ```bash
132
+ scripts/gh-org run <org> -- gh api user/memberships/orgs/<org> --jq .state
133
+ ```
134
+
135
+ - **`active`** access is real; the failure is something else.
136
+ - **`pending`** the invitation was never accepted. Accept it, then re-check.
137
+ - **the call fails** no logged-in account is a member. Add one (see below).
138
+
139
+ Once it reads `active`, pin the mapping so later work cannot pick a different account:
140
+
141
+ ```bash
142
+ scripts/gh-org map <org> <account>
143
+ ```
144
+
145
+ Note that **API access and SSH access are independent**. A healthy `gh` login says nothing
146
+ about whether pushes work; that needs an SSH key GitHub accepts for each org you push to.
147
+
148
+ ## Git identity setup (one time)
149
+
150
+ Per machine, once. Commits are attributed per repo, from the account that owns the org: never from a global identity, so a work repo can't be committed to with a personal address.
151
+
152
+ 1. **Drop the global identity.** Git then refuses to invent one from `$USER@$HOSTNAME`:
153
+ ```bash
154
+ git config --global --unset user.name
155
+ git config --global --unset user.email
156
+ git config --global user.useConfigOnly true
157
+ ```
158
+ Existing repos relying on the global value now have no identity, and their next commit fails until one is set. That failure is the point.
159
+
160
+ 2. **Cache the identity for each logged-in account** (`scripts/gh-org accounts` lists them):
161
+ ```bash
162
+ scripts/gh-org set-identity <account> "Full Name" you@example.com
163
+ ```
164
+ Stored as `gh-org.name.<account>` / `gh-org.email.<account>` in the global config. Git never reads these: they are a lookup table, not an identity.
165
+
166
+ 3. **Verify:** `scripts/gh-org doctor` prints `GLOBAL IDENTITY:`, `USECONFIGONLY:`, and one `IDENTITY:` line per account.
167
+
168
+ Then, in each repo: right after cloning, and any time a commit fails with *"Author identity unknown"*:
169
+
170
+ ```bash
171
+ scripts/gh-org apply # origin -> org -> account -> repo's --local user.name/user.email
172
+ ```
173
+
174
+ `scripts/gh-org identity [<org>]` prints what `apply` would write without touching the repo. The commit-time rules live in the `git-workflow` skill.
175
+
176
+ ## When something fails
177
+
178
+ Run `scripts/gh-org doctor`: it reports `GH:`, `BREW:`, `ACCOUNTS:`, the org mappings, and the git-identity state. Act on it:
179
+
180
+ - **`GH: MISSING`:** install it and retry:
181
+ ```bash
182
+ brew install gh
183
+ ```
184
+ - **`BREW: MISSING`:** Homebrew isn't set up. Install it from https://brew.sh at its standard prefix, then `brew install gh`.
185
+ - **`ACCOUNTS: NONE`**, or the target org has no account: add one (see below).
186
+ - **Everything `OK`:** auth is not the problem; read the actual error.
187
+
188
+ ## Adding an account
189
+
190
+ **Claude drives this; the user only approves in the browser.** Run the login in the **background** with stdin from `/dev/null` so `gh` uses the device-code flow instead of waiting on a TTY:
191
+
192
+ ```bash
193
+ gh auth login --hostname github.com --git-protocol ssh --scopes read:org,admin:public_key </dev/null
194
+ ```
195
+
196
+ Then grep the output for just the code and URL and relay them verbatim:
197
+
198
+ ```bash
199
+ grep -E 'one-time code|Open this URL' <output-file>
200
+ ```
201
+
202
+ `read:org` is required: the org-membership probe above depends on it. Monitor until `gh` exits, then re-run `scripts/gh-org doctor`.
203
+
204
+ Logging in a second account does **not** disturb the first; `gh` holds them all and `gh-org` selects between them per command.
205
+
206
+ ## Never print tokens
207
+
208
+ Do not run `gh auth token`, `gh auth status --show-token`, or `--with-token` with a pasted PAT: those put a live credential in the transcript. `gh-org` reads the token internally and exports it only into the child process; it never prints it. See `secrets`.
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ HOST="github.com"
5
+
6
+ die() { printf 'gh-org: %s\n' "$*" >&2; exit 1; }
7
+ note() { printf 'gh-org: %s\n' "$*" >&2; }
8
+ lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
9
+
10
+ need_gh() { command -v gh >/dev/null 2>&1 || die "gh is not installed: run: brew install gh"; }
11
+
12
+ accounts() {
13
+ gh auth status --hostname "$HOST" 2>&1 \
14
+ | grep -oE 'account [A-Za-z0-9._-]+' \
15
+ | awk '{print $2}'
16
+ }
17
+
18
+ token_for() { gh auth token --hostname "$HOST" --user "$1" 2>/dev/null; }
19
+
20
+ detect_org() {
21
+ local url owner
22
+ url=$(git remote get-url origin 2>/dev/null) || return 1
23
+ owner=$(printf '%s' "$url" | sed -E 's#^[a-z]+://[^/]+/##; s#^[^@]+@[^:]+:##; s#/.*$##')
24
+ [ -n "$owner" ] || return 1
25
+ printf '%s\n' "$owner"
26
+ }
27
+
28
+ resolve() {
29
+ local org="$1" cached u tok login state match="" here
30
+ here=$(git config --local --get github.user 2>/dev/null || true)
31
+ if [ -n "$here" ] && [ "$(lower "$(detect_org 2>/dev/null || true)")" = "$(lower "$org")" ]; then
32
+ printf '%s\n' "$here"; return 0
33
+ fi
34
+ cached=$(git config --global --get "gh-org.account.$(lower "$org")" 2>/dev/null || true)
35
+ if [ -n "$cached" ]; then printf '%s\n' "$cached"; return 0; fi
36
+
37
+ while read -r u; do
38
+ [ -n "$u" ] || continue
39
+ tok=$(token_for "$u") || continue
40
+ [ -n "$tok" ] || continue
41
+ login=$(GH_TOKEN="$tok" gh api user --jq .login 2>/dev/null || true)
42
+ if [ "$(lower "$login")" = "$(lower "$org")" ]; then match="$u"; break; fi
43
+ state=$(GH_TOKEN="$tok" gh api "user/memberships/orgs/${org}" --jq .state 2>/dev/null || true)
44
+ if [ "$state" = "active" ]; then match="$u"; break; fi
45
+ done < <(accounts)
46
+
47
+ [ -n "$match" ] || die "no logged-in gh account has access to '${org}': run: gh auth login"
48
+ git config --global "gh-org.account.$(lower "$org")" "$match"
49
+ note "mapped org '${org}' -> account '${match}'"
50
+ printf '%s\n' "$match"
51
+ }
52
+
53
+ cmd_identity() {
54
+ need_gh
55
+ local org="${1:-}" account name email
56
+ [ -n "$org" ] || org=$(detect_org) || die "could not detect org from the git remote: pass it explicitly"
57
+ account=$(resolve "$org")
58
+ name=$(git config --global --get "gh-org.name.$(lower "$account")" 2>/dev/null || true)
59
+ email=$(git config --global --get "gh-org.email.$(lower "$account")" 2>/dev/null || true)
60
+ printf 'ACCOUNT: %s\n' "$account"
61
+ if [ -z "$name" ] || [ -z "$email" ]; then
62
+ printf 'IDENTITY: NONE\n'
63
+ die "no identity cached for account '${account}': run: gh-org set-identity ${account} \"Full Name\" you@example.com"
64
+ fi
65
+ printf 'NAME: %s\nEMAIL: %s\n' "$name" "$email"
66
+ }
67
+
68
+ cmd_set_identity() {
69
+ local account="${1:-}" name="${2:-}" email="${3:-}"
70
+ [ -n "$account" ] && [ -n "$name" ] && [ -n "$email" ] \
71
+ || die 'usage: gh-org set-identity <account> "Full Name" <email>'
72
+ git config --global "gh-org.name.$(lower "$account")" "$name"
73
+ git config --global "gh-org.email.$(lower "$account")" "$email"
74
+ printf 'IDENTITY: %s -> %s <%s>\n' "$account" "$name" "$email"
75
+ }
76
+
77
+ cmd_apply() {
78
+ git rev-parse --git-dir >/dev/null 2>&1 || die "not inside a git repository"
79
+ local out name email
80
+ out=$(cmd_identity "$@")
81
+ name=$(printf '%s\n' "$out" | sed -n 's/^NAME: //p')
82
+ email=$(printf '%s\n' "$out" | sed -n 's/^EMAIL: //p')
83
+ git config --local user.name "$name"
84
+ git config --local user.email "$email"
85
+ printf 'LOCAL IDENTITY: %s <%s>\n' "$name" "$email"
86
+ }
87
+
88
+ cmd_doctor() {
89
+ if command -v gh >/dev/null 2>&1; then
90
+ printf 'GH: OK %s (%s)\n' "$(command -v gh)" "$(gh --version | head -1 | awk '{print $3}')"
91
+ else
92
+ printf 'GH: MISSING\n'
93
+ fi
94
+ if command -v brew >/dev/null 2>&1; then
95
+ printf 'BREW: OK %s\n' "$(command -v brew)"
96
+ else
97
+ printf 'BREW: MISSING\n'
98
+ fi
99
+ if command -v gh >/dev/null 2>&1; then
100
+ local list
101
+ list=$(accounts | tr '\n' ' ')
102
+ [ -n "${list// /}" ] && printf 'ACCOUNTS: %s\n' "$list" || printf 'ACCOUNTS: NONE\n'
103
+ fi
104
+ cmd_list
105
+ cmd_identities
106
+ }
107
+
108
+ cmd_identities() {
109
+ local gname gmail line
110
+ gname=$(git config --global --get user.name 2>/dev/null || true)
111
+ gmail=$(git config --global --get user.email 2>/dev/null || true)
112
+ if [ -n "$gname" ] || [ -n "$gmail" ]; then
113
+ printf 'GLOBAL IDENTITY: SET %s <%s>: remove it (see the git-workflow skill)\n' "$gname" "$gmail"
114
+ else
115
+ printf 'GLOBAL IDENTITY: NONE\n'
116
+ fi
117
+ if [ "$(git config --global --get user.useConfigOnly 2>/dev/null || true)" = "true" ]; then
118
+ printf 'USECONFIGONLY: OK\n'
119
+ else
120
+ printf 'USECONFIGONLY: OFF: run: git config --global user.useConfigOnly true\n'
121
+ fi
122
+ if ! git config --global --get-regexp '^gh-org\.email\.' >/dev/null 2>&1; then
123
+ printf 'IDENTITIES: NONE\n'; return 0
124
+ fi
125
+ git config --global --get-regexp '^gh-org\.email\.' | while read -r line; do
126
+ local acct; acct=$(printf '%s' "${line%% *}" | sed -E 's#^gh-org\.email\.##')
127
+ printf 'IDENTITY: %s -> %s <%s>\n' \
128
+ "$acct" "$(git config --global --get "gh-org.name.${acct}" 2>/dev/null || printf '?')" "${line#* }"
129
+ done
130
+ }
131
+
132
+ cmd_list() {
133
+ local line
134
+ if ! git config --global --get-regexp '^gh-org\.account\.' >/dev/null 2>&1; then
135
+ printf 'MAPPINGS: NONE\n'; return 0
136
+ fi
137
+ git config --global --get-regexp '^gh-org\.account\.' | while read -r line; do
138
+ printf 'MAPPING: %s -> %s\n' \
139
+ "$(printf '%s' "${line%% *}" | sed -E 's#^gh-org\.account\.##')" \
140
+ "${line#* }"
141
+ done
142
+ }
143
+
144
+ cmd_account() {
145
+ need_gh
146
+ local org="${1:-}"
147
+ [ -n "$org" ] || org=$(detect_org) || die "could not detect org from the git remote: pass it explicitly"
148
+ resolve "$org"
149
+ }
150
+
151
+ cmd_map() {
152
+ local org="${1:-}" login="${2:-}"
153
+ [ -n "$org" ] && [ -n "$login" ] || die "usage: gh-org map <org> <gh-account>"
154
+ git config --global "gh-org.account.$(lower "$org")" "$login"
155
+ printf 'MAPPING: %s -> %s\n' "$org" "$login"
156
+ }
157
+
158
+ cmd_unmap() {
159
+ local org="${1:-}"
160
+ [ -n "$org" ] || die "usage: gh-org unmap <org>"
161
+ git config --global --unset-all "gh-org.account.$(lower "$org")" 2>/dev/null || true
162
+ printf 'UNMAPPED: %s\n' "$org"
163
+ }
164
+
165
+ cmd_run() {
166
+ need_gh
167
+ local org=""
168
+ if [ "${1:-}" != "--" ]; then org="${1:-}"; shift || true; fi
169
+ [ "${1:-}" = "--" ] || die "usage: gh-org run [<org>] -- <command>..."
170
+ shift
171
+ [ $# -gt 0 ] || die "no command given"
172
+ [ -n "$org" ] || org=$(detect_org) || die "could not detect org from the git remote: pass it explicitly"
173
+
174
+ local user tok
175
+ user=$(resolve "$org")
176
+ tok=$(token_for "$user")
177
+ [ -n "$tok" ] || die "no token for account '${user}': run: gh auth login"
178
+ note "running as '${user}' for org '${org}'"
179
+ export GH_TOKEN="$tok"
180
+ unset GITHUB_TOKEN
181
+ exec "$@"
182
+ }
183
+
184
+ case "${1:-}" in
185
+ doctor) shift; cmd_doctor "$@" ;;
186
+ accounts) shift; need_gh; accounts ;;
187
+ account) shift; cmd_account "$@" ;;
188
+ list) shift; cmd_list ;;
189
+ identity) shift; cmd_identity "$@" ;;
190
+ set-identity) shift; cmd_set_identity "$@" ;;
191
+ apply) shift; cmd_apply "$@" ;;
192
+ map) shift; cmd_map "$@" ;;
193
+ unmap) shift; cmd_unmap "$@" ;;
194
+ run) shift; cmd_run "$@" ;;
195
+ *) die "usage: gh-org {doctor|accounts|account [<org>]|list|map <org> <account>|unmap <org>|identity [<org>]|set-identity <account> <name> <email>|apply [<org>]|run [<org>] -- <command>...}" ;;
196
+ esac