diff --git a/apps/gittensory-ui/src/lib/command-reference.ts b/apps/gittensory-ui/src/lib/command-reference.ts
new file mode 100644
index 0000000000..3b366525c9
--- /dev/null
+++ b/apps/gittensory-ui/src/lib/command-reference.ts
@@ -0,0 +1,7 @@
+// Generated by scripts/gen-command-reference.mjs. Do not edit manually.
+// Regenerate via `npm run command-reference`.
+export const PUBLIC_COMMAND_LIST =
+ "@gittensory help\n@gittensory ask\n@gittensory preflight\n@gittensory blockers\n@gittensory duplicate-check\n@gittensory miner-context\n@gittensory next-action\n@gittensory reviewability\n@gittensory repo-fit\n@gittensory packet";
+
+export const MAINTAINER_COMMAND_LIST =
+ "@gittensory queue-summary\n@gittensory confirmed-miners\n@gittensory review-now\n@gittensory needs-author\n@gittensory duplicate-clusters\n@gittensory burden-forecast\n@gittensory intake-health\n@gittensory outcome-patterns\n@gittensory noise-report";
diff --git a/apps/gittensory-ui/src/routes/docs.maintainer-install-trust.tsx b/apps/gittensory-ui/src/routes/docs.maintainer-install-trust.tsx
index 2cafe48e86..a818d7b75f 100644
--- a/apps/gittensory-ui/src/routes/docs.maintainer-install-trust.tsx
+++ b/apps/gittensory-ui/src/routes/docs.maintainer-install-trust.tsx
@@ -2,6 +2,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";
import { DocsPage } from "@/components/site/docs-page";
import { Callout, CodeBlock, FeatureRow } from "@/components/site/primitives";
+import { MAINTAINER_COMMAND_LIST, PUBLIC_COMMAND_LIST } from "@/lib/command-reference";
export const Route = createFileRoute("/docs/maintainer-install-trust")({
head: () => ({
@@ -137,14 +138,12 @@ POST /v1/repos/:owner/:repo/settings-preview`}
Maintainer commands should be treated like privileged review actions. Use them to fetch
context on demand, not to create always-on public scoring.
-
+
+
+ A separate maintainer-only queue-digest command family defaults to maintainers and
+ collaborators only:
+
+
If a command would include private reviewability, private scoreability, duplicate-risk, or
contributor-history context, the result must stay in maintainer-visible surfaces. Public
diff --git a/apps/gittensory-ui/src/routes/docs.maintainer-workflow.tsx b/apps/gittensory-ui/src/routes/docs.maintainer-workflow.tsx
index 7e9b85d3b6..0b78c2931d 100644
--- a/apps/gittensory-ui/src/routes/docs.maintainer-workflow.tsx
+++ b/apps/gittensory-ui/src/routes/docs.maintainer-workflow.tsx
@@ -3,6 +3,7 @@ import { createFileRoute, Link } from "@tanstack/react-router";
import { DocsPage } from "@/components/site/docs-page";
import { CodeBlock, Callout } from "@/components/site/primitives";
import { WorkflowMirror, type MirroredStep } from "@/components/site/workflow-mirror";
+import { MAINTAINER_COMMAND_LIST, PUBLIC_COMMAND_LIST } from "@/lib/command-reference";
export const Route = createFileRoute("/docs/maintainer-workflow")({
head: () => ({
@@ -149,14 +150,12 @@ GET /v1/repos/:owner/:repo/registration-readiness`}
Maintainers (and only maintainers) can trigger context with a comment. Output is scoped to
maintainer-visible packets when appropriate.
-
+
+
+ A separate maintainer-only queue-digest family defaults to maintainers and collaborators
+ only:
+
+
Public-facing comments are sanitized before they leave the Worker. Private scoring, reward,
diff --git a/package.json b/package.json
index 6e84b405ad..6ff8363098 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
"selfhost:postgres:migrate": "tsx scripts/migrate-selfhost-sqlite-to-postgres.ts",
"selfhost:env-reference": "node scripts/gen-selfhost-env-reference.mjs",
"selfhost:env-reference:check": "node scripts/gen-selfhost-env-reference.mjs --check",
+ "command-reference": "node scripts/gen-command-reference.mjs",
+ "command-reference:check": "node scripts/gen-command-reference.mjs --check",
"selfhost:validate-observability": "node scripts/validate-observability-configs.mjs",
"selfhost:config-lint": "tsx scripts/gittensory-config-lint.ts",
"cf-typegen": "wrangler types && perl -pi -e 's/[[:blank:]]+$//' worker-configuration.d.ts",
@@ -69,7 +71,7 @@
"test:smoke:observability": "node scripts/smoke-observability-traces.mjs",
"test:smoke:browser:install": "playwright install chromium",
"test:smoke:browser": "node scripts/smoke-ui-browser.mjs",
- "test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run typecheck && npm run test:coverage && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
+ "test:ci": "git diff --check && npm run actionlint && npm run db:migrations:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run typecheck && npm run test:coverage && npm run test:workers && npm run build:mcp && npm run test:mcp-pack && npm run build:miner && npm run rees:test && npm run ui:openapi:check && npm run ui:openapi:settings-parity && npm run ui:version-audit && npm run command-reference:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
"test:release": "npm run test:ci && npm run changelog:check",
"test:release:mcp": "npm run test:ci && npm run changelog:check:mcp",
"test:watch": "vitest",
diff --git a/scripts/gen-command-reference.d.mts b/scripts/gen-command-reference.d.mts
new file mode 100644
index 0000000000..d7984b4969
--- /dev/null
+++ b/scripts/gen-command-reference.d.mts
@@ -0,0 +1,39 @@
+export type CommandCatalogEntry = {
+ id: string;
+ title: string;
+ description: string;
+};
+
+export type CommandCatalogOptions = {
+ rootDir?: string;
+ sourcePath?: string;
+};
+
+export type WriteCommandReferenceOptions = CommandCatalogOptions & {
+ outputPath?: string;
+ check?: boolean;
+};
+
+export declare const DEFAULT_SOURCE_PATH: string;
+export declare const DEFAULT_OUTPUT_PATH: string;
+
+export declare function extractCatalogEntries(sourceText: string, catalogConstName: string): CommandCatalogEntry[];
+
+export declare function renderCommandList(entries: CommandCatalogEntry[]): string;
+
+export declare function collectCommandCatalogs(options?: CommandCatalogOptions): {
+ publicCommands: CommandCatalogEntry[];
+ maintainerCommands: CommandCatalogEntry[];
+};
+
+export declare function renderCommandReferenceModule(catalogs: {
+ publicCommands: CommandCatalogEntry[];
+ maintainerCommands: CommandCatalogEntry[];
+}): string;
+
+export declare function writeCommandReference(options?: WriteCommandReferenceOptions): {
+ changed: boolean;
+ outputPath: string;
+ publicCommands: CommandCatalogEntry[];
+ maintainerCommands: CommandCatalogEntry[];
+};
diff --git a/scripts/gen-command-reference.mjs b/scripts/gen-command-reference.mjs
new file mode 100644
index 0000000000..23604f34df
--- /dev/null
+++ b/scripts/gen-command-reference.mjs
@@ -0,0 +1,102 @@
+#!/usr/bin/env node
+// Generates the @gittensory command reference from its single source of truth --
+// src/github/commands.ts's PUBLIC_MENTION_COMMAND_CATALOG and MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG
+// -- so docs pages render the real command list instead of hand-copying it (a hand-copy is exactly how
+// the 9-command maintainer-only queue-digest family went completely undocumented; see #3046). Mirrors
+// scripts/gen-selfhost-env-reference.mjs's generate/--check dual-mode convention.
+import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import { dirname, resolve } from "node:path";
+import { pathToFileURL } from "node:url";
+
+export const DEFAULT_SOURCE_PATH = "src/github/commands.ts";
+export const DEFAULT_OUTPUT_PATH = "apps/gittensory-ui/src/lib/command-reference.ts";
+
+const PUBLIC_CATALOG_NAME = "PUBLIC_MENTION_COMMAND_CATALOG";
+const MAINTAINER_CATALOG_NAME = "MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG";
+
+// Self-defense floor: 10 public + 9 maintainer-only commands exist today. If extraction ever finds fewer
+// than 15 total entries across both catalogs, the extraction regex is almost certainly broken -- fail
+// loudly instead of silently writing an empty/tiny reference (mirrors check-docs-drift.mjs's own floor).
+const MIN_TOTAL_COMMANDS = 15;
+
+/** Find the array literal assigned to `const = [ ... ] as const;` (non-greedy up to
+ * the FIRST `] as const;` after the const name -- catalogs in commands.ts never nest another
+ * `] as const;` inside themselves, so the first close is always the right one) and extract every
+ * `{ id: "...", title: "...", description: "..." }` entry from within that slice, in source order.
+ * Scoped to the named catalog's own slice so two catalogs in the same file never bleed into each
+ * other's entries. */
+export function extractCatalogEntries(sourceText, catalogConstName) {
+ const catalogPattern = new RegExp(`const\\s+${catalogConstName}\\s*=\\s*\\[([\\s\\S]*?)\\]\\s*as\\s*const;`);
+ const catalogMatch = catalogPattern.exec(sourceText);
+ if (!catalogMatch) return [];
+ const entryPattern = /id:\s*"([^"]+)"\s*,\s*title:\s*"([^"]+)"\s*,\s*description:\s*"([^"]+)"/g;
+ return [...catalogMatch[1].matchAll(entryPattern)].map((match) => ({
+ id: match[1],
+ title: match[2],
+ description: match[3],
+ }));
+}
+
+/** Renders a catalog's entries as the same `@gittensory ` line format the docs pages already
+ * hand-write, newline-joined, in catalog order. */
+export function renderCommandList(entries) {
+ return entries.map((entry) => `@gittensory ${entry.id}`).join("\n");
+}
+
+export function collectCommandCatalogs({ rootDir = process.cwd(), sourcePath = DEFAULT_SOURCE_PATH } = {}) {
+ const sourceText = readFileSync(resolve(rootDir, sourcePath), "utf8");
+ const publicCommands = extractCatalogEntries(sourceText, PUBLIC_CATALOG_NAME);
+ const maintainerCommands = extractCatalogEntries(sourceText, MAINTAINER_CATALOG_NAME);
+ const total = publicCommands.length + maintainerCommands.length;
+ if (total < MIN_TOTAL_COMMANDS) {
+ throw new Error(
+ `gen-command-reference: extraction found only ${total} total @gittensory command(s) across both catalogs in ${sourcePath} -- expected ${MIN_TOTAL_COMMANDS}+; the extraction regex may be broken`,
+ );
+ }
+ return { publicCommands, maintainerCommands };
+}
+
+export function renderCommandReferenceModule({ publicCommands, maintainerCommands }) {
+ return `// Generated by scripts/gen-command-reference.mjs. Do not edit manually.
+// Regenerate via \`npm run command-reference\`.
+export const PUBLIC_COMMAND_LIST =
+ ${JSON.stringify(renderCommandList(publicCommands))};
+
+export const MAINTAINER_COMMAND_LIST =
+ ${JSON.stringify(renderCommandList(maintainerCommands))};
+`;
+}
+
+export function writeCommandReference({
+ rootDir = process.cwd(),
+ sourcePath = DEFAULT_SOURCE_PATH,
+ outputPath = DEFAULT_OUTPUT_PATH,
+ check = false,
+} = {}) {
+ const { publicCommands, maintainerCommands } = collectCommandCatalogs({ rootDir, sourcePath });
+ const output = renderCommandReferenceModule({ publicCommands, maintainerCommands });
+ const absOutput = resolve(rootDir, outputPath);
+ const current = existsSync(absOutput) ? readFileSync(absOutput, "utf8") : null;
+ const changed = current !== output;
+ if (!check && changed) {
+ mkdirSync(dirname(absOutput), { recursive: true });
+ writeFileSync(absOutput, output);
+ }
+ return { changed, outputPath, publicCommands, maintainerCommands };
+}
+
+function main(argv) {
+ const check = argv.includes("--check");
+ const result = writeCommandReference({ check });
+ if (check && result.changed) {
+ process.stderr.write(`gen-command-reference: ${result.outputPath} is stale; run npm run command-reference.\n`);
+ process.exit(1);
+ }
+ process.stdout.write(
+ `gen-command-reference: ${check ? "checked" : "wrote"} ${result.publicCommands.length} public + ${result.maintainerCommands.length} maintainer-only command references in ${result.outputPath}\n`,
+ );
+}
+
+if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
+ main(process.argv.slice(2));
+}
diff --git a/test/unit/gen-command-reference-script.test.ts b/test/unit/gen-command-reference-script.test.ts
new file mode 100644
index 0000000000..af8b1717bb
--- /dev/null
+++ b/test/unit/gen-command-reference-script.test.ts
@@ -0,0 +1,157 @@
+import { execFileSync } from "node:child_process";
+import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join, resolve } from "node:path";
+import { describe, expect, it } from "vitest";
+import {
+ type CommandCatalogEntry,
+ collectCommandCatalogs,
+ DEFAULT_OUTPUT_PATH,
+ extractCatalogEntries,
+ renderCommandList,
+ renderCommandReferenceModule,
+} from "../../scripts/gen-command-reference.mjs";
+
+function fixtureRoot(commandsSource: string): string {
+ const root = mkdtempSync(join(tmpdir(), "gt-command-reference-"));
+ mkdirSync(join(root, "src", "github"), { recursive: true });
+ writeFileSync(join(root, "src", "github", "commands.ts"), commandsSource);
+ return root;
+}
+
+describe("gen-command-reference script (#3046)", () => {
+ describe("extractCatalogEntries", () => {
+ const fixture = `
+ const PUBLIC_MENTION_COMMAND_CATALOG = [
+ { id: "help", title: "Help", description: "Show help." },
+ { id: "ask", title: "Ask", description: "Answer a question." },
+ ] as const;
+
+ const MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG = [
+ { id: "queue-summary", title: "Queue summary", description: "Post a queue digest." },
+ ] as const;
+ `;
+
+ it("extracts only the entries from the named catalog, not the other one, in source order", () => {
+ expect(extractCatalogEntries(fixture, "PUBLIC_MENTION_COMMAND_CATALOG")).toEqual([
+ { id: "help", title: "Help", description: "Show help." },
+ { id: "ask", title: "Ask", description: "Answer a question." },
+ ]);
+ expect(extractCatalogEntries(fixture, "MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG")).toEqual([
+ { id: "queue-summary", title: "Queue summary", description: "Post a queue digest." },
+ ]);
+ });
+
+ it("returns an empty array when the named catalog does not exist", () => {
+ expect(extractCatalogEntries(fixture, "MISSING_CATALOG")).toEqual([]);
+ });
+ });
+
+ describe("renderCommandList", () => {
+ it("renders one @gittensory line per entry, newline-joined, in order", () => {
+ expect(
+ renderCommandList([
+ { id: "help", title: "Help", description: "Show help." },
+ { id: "ask", title: "Ask", description: "Answer a question." },
+ ]),
+ ).toBe("@gittensory help\n@gittensory ask");
+ });
+
+ it("renders an empty string for an empty catalog", () => {
+ expect(renderCommandList([])).toBe("");
+ });
+ });
+
+ describe("collectCommandCatalogs", () => {
+ it("self-defends against a broken extraction regex (fewer than 15 total commands)", () => {
+ const root = fixtureRoot(`
+ const PUBLIC_MENTION_COMMAND_CATALOG = [{ id: "only-one", title: "Only", description: "One." }] as const;
+ const MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG = [] as const;
+ `);
+
+ expect(() => collectCommandCatalogs({ rootDir: root })).toThrow(/extraction regex may be broken/);
+ });
+
+ it("extracts a fixture with exactly 15 total commands without throwing", () => {
+ const publicIds = Array.from({ length: 10 }, (_, i) => `{ id: "public-${i}", title: "Public ${i}", description: "Desc ${i}." },`).join("\n");
+ const maintainerIds = Array.from({ length: 5 }, (_, i) => `{ id: "maint-${i}", title: "Maint ${i}", description: "Desc ${i}." },`).join("\n");
+ const root = fixtureRoot(`
+ const PUBLIC_MENTION_COMMAND_CATALOG = [
+ ${publicIds}
+ ] as const;
+ const MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG = [
+ ${maintainerIds}
+ ] as const;
+ `);
+
+ const { publicCommands, maintainerCommands } = collectCommandCatalogs({ rootDir: root });
+ expect(publicCommands).toHaveLength(10);
+ expect(maintainerCommands).toHaveLength(5);
+ });
+
+ it("extracts the real 10 public + 9 maintainer-only commands from the real repo source", () => {
+ const { publicCommands, maintainerCommands } = collectCommandCatalogs({ rootDir: process.cwd() });
+
+ expect(publicCommands).toHaveLength(10);
+ expect(maintainerCommands).toHaveLength(9);
+ expect(publicCommands.map((c: CommandCatalogEntry) => c.id)).toEqual([
+ "help",
+ "ask",
+ "preflight",
+ "blockers",
+ "duplicate-check",
+ "miner-context",
+ "next-action",
+ "reviewability",
+ "repo-fit",
+ "packet",
+ ]);
+ expect(maintainerCommands.map((c: CommandCatalogEntry) => c.id)).toEqual([
+ "queue-summary",
+ "confirmed-miners",
+ "review-now",
+ "needs-author",
+ "duplicate-clusters",
+ "burden-forecast",
+ "intake-health",
+ "outcome-patterns",
+ "noise-report",
+ ]);
+ });
+ });
+
+ describe("renderCommandReferenceModule", () => {
+ it("renders a generated-file header plus both exported string constants", () => {
+ const output = renderCommandReferenceModule({
+ publicCommands: [{ id: "help", title: "Help", description: "Show help." }],
+ maintainerCommands: [{ id: "queue-summary", title: "Queue summary", description: "Post a digest." }],
+ });
+
+ expect(output).toContain("// Generated by scripts/gen-command-reference.mjs. Do not edit manually.");
+ expect(output).toContain("npm run command-reference");
+ expect(output).toContain('export const PUBLIC_COMMAND_LIST =\n "@gittensory help";');
+ expect(output).toContain('export const MAINTAINER_COMMAND_LIST =\n "@gittensory queue-summary";');
+ });
+ });
+
+ // Most important regression test in this file: proves the committed generated file on disk is
+ // byte-identical to what the generator's pure function would produce right now from the real
+ // src/github/commands.ts. If this fails, either the generated file is stale (run
+ // `npm run command-reference` and commit it) or the extraction logic changed -- either way, the
+ // generated file must not be hand-edited to make this test pass.
+ it("the committed generated file matches what the generator would produce right now (regression guard)", () => {
+ const rootDir = process.cwd();
+ const { publicCommands, maintainerCommands } = collectCommandCatalogs({ rootDir });
+ const expected = renderCommandReferenceModule({ publicCommands, maintainerCommands });
+
+ const actual = readFileSync(resolve(rootDir, DEFAULT_OUTPUT_PATH), "utf8");
+
+ expect(actual).toBe(expected);
+ });
+
+ it("prints a clean summary and exits 0 for the real repo state when run as a subprocess with --check", () => {
+ const output = execFileSync("node", ["scripts/gen-command-reference.mjs", "--check"], { encoding: "utf8" });
+
+ expect(output).toMatch(/gen-command-reference: checked \d+ public \+ \d+ maintainer-only command references in/);
+ });
+});