Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions docs/reference/cli-options.md

Large diffs are not rendered by default.

67 changes: 44 additions & 23 deletions docs/usage/team-grouping.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Team grouping

`--group-by-team-prefix` organises result repositories by their GitHub team membership, as a **hierarchy** of headings. It is especially useful in large organisations with multiple gammes, chapters or squads.
`--group-by-team-prefix` organises result repositories by their GitHub team membership, as a **hierarchy** of headings. It is especially useful in large organisations with multiple tribes, chapters or squads.

## Prerequisites

Expand All @@ -25,18 +25,18 @@ The value of `--group-by-team-prefix` is a small grammar:
- `,` separates **independent chains** — each is grouped on its own, in order, against whatever repos the previous chains haven't already claimed.

```bash
# One 2-level chain: group by gamme- first, then by squad- within each gamme
# One 2-level chain: group by tribe- first, then by squad- within each gamme
github-code-search "useFeatureFlag" --org fulll \
--group-by-team-prefix gamme-/squad-
--group-by-team-prefix tribe-/squad-
```

```bash
# A 2-level chain (gamme-/squad-) plus an independent 1-level chain (chapter-)
# A 2-level chain (tribe-/squad-) plus an independent 1-level chain (chapter-)
github-code-search "useFeatureFlag" --org fulll \
--group-by-team-prefix gamme-/squad-,chapter-
--group-by-team-prefix tribe-/squad-,chapter-
```

A chain can have as many levels as you need (`gamme-/squad-/chapter-`, …). Malformed segments (a stray leading/trailing/double `,` or `/`) are dropped with a warning on stderr rather than silently producing an empty prefix.
A chain can have as many levels as you need (`tribe-/squad-/chapter-`, …). Malformed segments (a stray leading/trailing/double `,` or `/`) are dropped with a warning on stderr rather than silently producing an empty prefix.

## Grouping algorithm

Expand All @@ -47,20 +47,23 @@ Within **one level** of a chain, repos are bucketed exactly the same way regardl
3. Repos belonging to **3+** matching teams → same, in ascending combination-size order.
4. Repos matching **no team** at this level → collected into an `other` section.

Before bucketing, a team that is a prefix of another team **already matched by the same repo** is dropped from that repo's matching set (e.g. `chapter-architect-a` is redundant when `chapter-architect` is also present) — the broader team already implies the narrower one, so keeping both would only inflate the combined-section label.

Then, for a chain with more levels, **every section produced above is recursively sub-grouped** by the next prefix — including its own `other` bucket, which becomes a nested `other` at the next depth.

Every level of a chain is tried in order against whatever repos the _earlier levels of that same chain_ haven't already claimed — a repo that only matches `squad-` (not `tribe-`) in a `tribe-/squad-` chain still gets its own top-level section from `squad-`, instead of being invisible to the chain and falling through to a later chain or `other`.

Independent chains (separated by `,`) are processed in order, each consuming repos from the pool not yet claimed by an earlier chain. Repos matched by no chain at all end up in a single top-level `other` section.

### Automatic nesting of overlapping team names
### Automatic combining of overlapping team names

Within one level, if a team's name is a **prefix of another team's name** (e.g. `gamme-lead-client` and `gamme-lead-client-p1`), the tool nests the more specific team under the more general one automatically — instead of listing them as unrelated siblings:
Within one level, if a team's name is a **prefix of another team's name** (e.g. `tribe-a` and `tribe-a-p1`), the tool combines them into one section automatically — instead of listing them as unrelated siblings or nesting one under the other:

```text
## gamme-lead-client
### gamme-lead-client-p1
## tribe-a + tribe-a-p1
```

This cascades across any number of overlapping names, and applies independently at every depth of a chain.
This cascades across any number of overlapping names (all merging into one section), and applies independently at every depth of a chain. The combined section behaves exactly like a multi-team combo — `--pick-team` and [`--pick-team-auto`](#auto-pick-by-common-prefix) can resolve it the same way.

## Non-interactive output

Expand Down Expand Up @@ -90,20 +93,20 @@ This cascades across any number of overlapping names, and applies independently
- [ ] [src/legacy.js:5:1](https://github.com/fulll/legacy-monolith/blob/main/src/legacy.js#L5)
```

### Nested (`gamme-/squad-`) output
### Nested (`tribe-/squad-`) output

Nested levels render as consecutive markdown headings (`##`, `###`, `####`, …, capped at H6) — a sibling section that shares an ancestor with the previous one doesn't repeat that ancestor's heading:

```text
7 repos · 7 files · 8 matches selected

## gamme-lead-client
### squad-bank
## tribe-a
### squad-a

- **fulll/bank** (1 match)
- [ ] [src/index.ts:3:14](https://github.com/fulll/bank/blob/main/src/index.ts#L3)

## gamme-lead-mobile
## tribe-b
### squad-core + squad-mobile

- **fulll/tools-mobile** (1 match)
Expand All @@ -129,7 +132,7 @@ Each result carries its full hierarchy path (root first) in a `section` array:
"results": [
{
"repo": "fulll/tools-mobile",
"section": ["gamme-lead-mobile", "squad-core + squad-mobile"],
"section": ["tribe-b", "squad-core + squad-mobile"],
"matches": [{ "path": "src/index.ts", "url": "...", "line": 1, "col": 1 }]
}
]
Expand All @@ -141,10 +144,10 @@ Each result carries its full hierarchy path (root first) in a `section` array:
In the TUI, team sections appear as separator lines between repository rows, indented by 2 spaces per nesting level:

```text
── gamme-lead-client
── squad-bank
── tribe-a
── squad-a
▶ ◉ fulll/bank (1 match)
── gamme-lead-mobile
── tribe-b
── squad-core + squad-mobile
▶ ◉ fulll/tools-mobile (1 match)
── other
Expand Down Expand Up @@ -193,7 +196,7 @@ The combined label can be:
- **A fully-qualified path**, joined with `>`, when the label is ambiguous or you'd rather be explicit:

```bash
--pick-team "gamme-lead-client > squad-a + squad-b"=squad-a
--pick-team "tribe-a > squad-a + squad-b"=squad-a
```

The flag is repeatable — add one `--pick-team` per combined section to resolve. The replay command emits `--pick-team` automatically (with a fully-qualified path when the pick was made on a nested section) when a pick was confirmed in the TUI.
Expand All @@ -204,23 +207,41 @@ If the combined label or path is not found (typo, ambiguous, or the section was

## Auto-pick by common prefix

Many combined sections aren't actually ambiguous: when one of the team names is a literal prefix of every other team name in the combo (e.g. `gamme-lead-client` and `gamme-lead-client-p1`), the "parent" team is the obvious owner. `--pick-team-auto` resolves these automatically, without needing a manual `--pick-team`:
Many combined sections aren't actually ambiguous: when one of the team names is a literal prefix of every other team name in the combo (e.g. `tribe-a` and `tribe-a-p1`), the "parent" team is the obvious owner. `--pick-team-auto` resolves these automatically, without needing a manual `--pick-team`:

```bash
github-code-search query "useFeatureFlag" --org fulll \
--group-by-team-prefix gamme- \
--group-by-team-prefix tribe- \
--pick-team-auto
```

```text
## gamme-lead-client + gamme-lead-client-p1 → ## gamme-lead-client
## tribe-a + tribe-a-p1 → ## tribe-a
```

- Combos with **no common-prefix team** (e.g. `squad-frontend + squad-mobile` — neither is a prefix of the other) are left combined and unresolved, exactly like today.
- Applies independently **at every hierarchy depth**, not just the top level.
- An explicit `--pick-team` for the same section always wins: run explicit picks first, then `--pick-team-auto` resolves whatever combined sections remain.
- The replay command emits `--pick-team-auto` when it was used, so a session is reproduced exactly.

## Excluding noisy team prefixes

Some orgs have many closely related, deeply-overlapping team names under one prefix (e.g. `chapter-validators-core`, `chapter-validators-client`, `chapter-validators-frontend-client`, ...). When several of these co-occur on the same repos, `--group-by-team-prefix` produces many distinct combined sections that neither `--pick-team-auto` nor manual `--pick-team` can cleanly resolve, since no single team name is a common prefix of the others.

`--exclude-team-prefixes` removes matching teams from consideration **before** grouping runs, reducing ambiguous combos at the source:

```bash
github-code-search query "useFeatureFlag" --org fulll \
--group-by-team-prefix chapter- \
--exclude-team-prefixes chapter-validators- \
--pick-team-auto
```

- Comma-separated, same syntax as `--exclude-repositories` / `--exclude-extracts`.
- A repo left with **no matching team** after exclusion falls into `other`, exactly like a repo with no matching team today.
- Only applies with `--group-by-team-prefix`; a warning is emitted (and the flag is a no-op) otherwise.
- The replay command emits `--exclude-team-prefixes` when it was used, so a session is reproduced exactly.

## Re-pick & undo pick

After using `--pick-team` (or the interactive `p` shortcut) to assign a combined section to a team, individual repos marked `◈` can be re-assigned or restored to their original combined section at any time — regardless of how deeply nested the original section was.
Expand Down
46 changes: 39 additions & 7 deletions github-code-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { buildOutput } from "./src/output.ts";
import {
applyTeamPickInTree,
autoPickTeamsByCommonPrefix,
excludeTeamsByPrefix,
findCombinedSectionPaths,
flattenTeamHierarchy,
groupByTeamHierarchy,
Expand Down Expand Up @@ -78,7 +79,7 @@ function colorDesc(s: string): string {
// would make Commander's Help.preformatted() (newline followed by
// whitespace) treat the WHOLE description as already manually indented
// and skip aligning continuation lines to the option column.
if (/^\s*(e\.g\.|repoA|myorg\/|squad-|chapter-|gamme-)/.test(line)) return style.dim(line);
if (/^\s*(e\.g\.|repoA|myorg\/|squad-|chapter-|tribe-)/.test(line)) return style.dim(line);
// Colorize any remaining bare URL (http/https) anywhere in the line
return line.replace(/(https?:\/\/\S+)/g, (url) => style.style(["cyan", "underline"], url));
})
Expand Down Expand Up @@ -180,24 +181,37 @@ function addSearchOptions(cmd: Command): Command {
"--group-by-team-prefix <prefixes>",
[
"Comma-separated team-name prefixes used to group result repos by GitHub team.",
"Use / within one entry to nest levels: gamme-/squad- groups by gamme- first,",
"Use / within one entry to nest levels: tribe-/squad- groups by tribe- first,",
"then sub-groups each section by squad-. Combine independent chains with ,:",
"gamme-/squad-,chapter-",
"tribe-/squad-,chapter-",
"Repos are first grouped by single-team match, then multi-team, then the next",
"level. Repos matching no prefix go into 'other'. Team names that overlap",
"(e.g. squad-a and squad-a-legacy) are nested automatically.",
"(e.g. squad-a and squad-a-legacy) are combined automatically.",
"Docs: https://fulll.github.io/github-code-search/usage/team-grouping",
].join("\n"),
"",
)
.option(
"--exclude-team-prefixes <prefixes>",
[
"Comma-separated team-name prefixes to exclude from grouping entirely,",
"before combined sections are formed. Useful to filter out noisy/overly",
"granular team names (e.g. many chapter-validators-* sub-teams) that",
"would otherwise produce combined sections --pick-team-auto can't resolve.",
"A repo left with no matching team after exclusion falls into 'other'.",
"Only applies with --group-by-team-prefix.",
"Docs: https://fulll.github.io/github-code-search/usage/team-grouping#excluding-noisy-team-prefixes",
].join("\n"),
"",
)
.option(
"--pick-team <assignment>",
[
"Assign a combined team section to a single owner.",
'Format: "combined label"=chosenTeam (the = separator is required).',
'Example: --pick-team "squad-frontend + squad-mobile"=squad-frontend',
"The combined label may be unqualified (auto-resolved when unambiguous)",
'or a full path when nested / ambiguous: "gamme-client > squad-a + squad-b"=squad-a',
'or a full path when nested / ambiguous: "tribe-a > squad-a + squad-b"=squad-a',
"Repeatable — one flag per combined section to resolve.",
"Only applies with --group-by-team-prefix.",
"Docs: https://fulll.github.io/github-code-search/usage/team-grouping#team-pick-mode",
Expand All @@ -209,8 +223,8 @@ function addSearchOptions(cmd: Command): Command {
"--pick-team-auto",
[
"Auto-resolve combined team sections whose team names share a common",
'prefix (e.g. "gamme-lead-client + gamme-lead-client-p1" \u2192 auto-picks',
'"gamme-lead-client"), without needing an explicit --pick-team.',
'prefix (e.g. "tribe-a + tribe-a-p1" \u2192 auto-picks',
'"tribe-a"), without needing an explicit --pick-team.',
"Combos with no common-prefix team (e.g. squad-a + squad-b) are left",
"unresolved. An explicit --pick-team for the same section always wins.",
"Applies at every hierarchy depth. Only applies with --group-by-team-prefix.",
Expand Down Expand Up @@ -246,6 +260,7 @@ async function searchAction(
includeArchived: boolean;
excludeTemplateRepositories: boolean;
groupByTeamPrefix: string;
excludeTeamPrefixes?: string;
pickTeam: string[];
pickTeamAuto?: boolean;
cache: boolean;
Expand Down Expand Up @@ -377,6 +392,11 @@ async function searchAction(
);
}
}
if (!opts.groupByTeamPrefix && opts.excludeTeamPrefixes) {
process.stderr.write(
"warning: --exclude-team-prefixes requires --group-by-team-prefix; skipping\n",
);
}
if (opts.groupByTeamPrefix) {
const { chains, warnings: chainWarnings } = parseTeamPrefixChains(opts.groupByTeamPrefix);
for (const w of chainWarnings) process.stderr.write(`warning: ${w}\n`);
Expand All @@ -395,6 +415,16 @@ async function searchAction(
g.teams = teamMap.get(g.repoFullName) ?? [];
}

const excludeTeamPrefixes = opts.excludeTeamPrefixes
? opts.excludeTeamPrefixes
.split(",")
.map((p) => p.trim())
.filter((p) => p.length > 0)
: [];
if (excludeTeamPrefixes.length > 0) {
groups = excludeTeamsByPrefix(groups, excludeTeamPrefixes);
}

let sections = groupByTeamHierarchy(groups, chains);

for (const assignment of opts.pickTeam) {
Expand Down Expand Up @@ -436,6 +466,7 @@ async function searchAction(
includeArchived,
excludeTemplates,
groupByTeamPrefix: opts.groupByTeamPrefix,
excludeTeamPrefixes: opts.excludeTeamPrefixes,
pickTeamAuto: opts.pickTeamAuto,
regexHint: opts.regexHint,
pickTeams: Object.keys(pickTeams).length > 0 ? pickTeams : undefined,
Expand Down Expand Up @@ -497,6 +528,7 @@ async function searchAction(
includeArchived,
excludeTemplates,
opts.groupByTeamPrefix,
opts.excludeTeamPrefixes ?? "",
Boolean(opts.pickTeamAuto),
opts.regexHint ?? "",
Object.keys(pickTeams).length > 0 ? pickTeams : {},
Expand Down
12 changes: 12 additions & 0 deletions src/completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ describe("generateCompletion", () => {
expect(generateCompletion("bash")).toContain("--pick-team-auto");
});

it("contains --exclude-team-prefixes", () => {
expect(generateCompletion("bash")).toContain("--exclude-team-prefixes");
});

it("contains format values (markdown, json)", () => {
const script = generateCompletion("bash");
expect(script).toContain("markdown");
Expand Down Expand Up @@ -83,6 +87,10 @@ describe("generateCompletion", () => {
expect(generateCompletion("zsh")).toContain("--pick-team-auto");
});

it("contains --exclude-team-prefixes", () => {
expect(generateCompletion("zsh")).toContain("--exclude-team-prefixes");
});

it("contains a 'compdef' directive (zsh-style)", () => {
const script = generateCompletion("zsh");
expect(script).toContain("compdef ");
Expand Down Expand Up @@ -119,6 +127,10 @@ describe("generateCompletion", () => {
expect(generateCompletion("fish")).toContain("pick-team-auto");
});

it("contains exclude-team-prefixes", () => {
expect(generateCompletion("fish")).toContain("exclude-team-prefixes");
});

it("uses fish 'complete -c' syntax", () => {
const script = generateCompletion("fish");
expect(script).toContain("complete -c github-code-search");
Expand Down
6 changes: 6 additions & 0 deletions src/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ const OPTIONS = [
takesArg: true,
values: [],
},
{
flag: "exclude-team-prefixes",
description: "Comma-separated team-name prefixes to exclude from grouping",
takesArg: true,
values: [],
},
{
flag: "pick-team",
description: "Assign a combined team section to a single owner (repeatable)",
Expand Down
Loading
Loading