+ >
+ );
+}
+
+function GittensoryCommandsReference() {
+ return (
+
+
+ Commands never flip the gate to advisory and never bypass the one-shot disposition.{" "}
+ pause and resume affect only auto-review scheduling — not gate enforcement.
+ See How reviews work for the gate/review split.
+
+
+
Syntax
+
+ Post a comment on a pull request (or issue thread) mentioning @gittensory followed by a
+ verb. Trailing free text becomes the command argument where noted (for example{" "}
+ @gittensory ask what should I fix first?).
+
+ [argument or reason]`} />
+
+
+
+
+
+
Per-command authorization overrides
+
+ Default allowed roles ship in the worker configuration. A maintainer can tighten or widen a single verb
+ via commandAuthorization in .gittensory.yml (resolved in the same order as
+ other per-repo settings: manifest → database → defaults).
+
+
+
+ Maintainer-only digest verbs ignore a plain pr_author role even when widened — only
+ maintainer, collaborator, and confirmed_miner survive the clamp for those commands.
+
+
+
Related docs
+
+
+ Maintainer workflow — when to invoke commands in a PR
+ thread
+
+
+ How reviews work — gate, dual-AI review, and unified comment
+
+
+ Tuning your reviews — per-repo review and agent execution modes
+
+
+
+ );
+}
diff --git a/apps/gittensory-ui/src/routes/docs.index.tsx b/apps/gittensory-ui/src/routes/docs.index.tsx
index f030bf1632..d57d64eeed 100644
--- a/apps/gittensory-ui/src/routes/docs.index.tsx
+++ b/apps/gittensory-ui/src/routes/docs.index.tsx
@@ -74,6 +74,7 @@ const AUDIENCES: Audience[] = [
{ to: "/docs/maintainer-install-trust", label: "Install & trust guide" },
{ to: "/docs/github-app", label: "GitHub App configuration" },
{ to: "/docs/maintainer-workflow", label: "Maintainer workflow" },
+ { to: "/docs/gittensory-commands", label: "@gittensory commands" },
{ to: "/docs/self-hosting-rees-analyzers", label: "REES analyzers" },
{ to: "/docs/upstream-drift", label: "Upstream drift" },
{ to: "/docs/ai-summaries", label: "AI summaries policy" },
diff --git a/apps/gittensory-ui/src/routes/docs.maintainer-workflow.tsx b/apps/gittensory-ui/src/routes/docs.maintainer-workflow.tsx
index 2e0eeedbf6..0000ce2f8a 100644
--- a/apps/gittensory-ui/src/routes/docs.maintainer-workflow.tsx
+++ b/apps/gittensory-ui/src/routes/docs.maintainer-workflow.tsx
@@ -158,6 +158,10 @@ GET /v1/repos/:owner/:repo/registration-readiness`}
only:
+
+ For syntax, default roles, PR action verbs, and the gate vs auto-review boundary, see the{" "}
+ @gittensory command reference.
+
Public-facing comments are sanitized before they leave the Worker. Private scoring, reward,
diff --git a/scripts/check-docs-drift.mjs b/scripts/check-docs-drift.mjs
index df60673f22..c9f6346a5b 100644
--- a/scripts/check-docs-drift.mjs
+++ b/scripts/check-docs-drift.mjs
@@ -109,7 +109,7 @@ export function checkDocsDrift({ root, readFile = defaultReadFile }) {
if (allCommandIds.length < 15) {
failures.push(`src/github/commands.ts: extraction found only ${allCommandIds.length} unique @gittensory command ids -- expected 15+; the extraction regex may be broken`);
} else {
- const commandDocsPages = ["docs.maintainer-workflow.tsx", "docs.maintainer-install-trust.tsx"];
+ const commandDocsPages = ["docs.maintainer-workflow.tsx", "docs.maintainer-install-trust.tsx", "docs.gittensory-commands.tsx"];
for (const page of commandDocsPages) {
const pageText = read(`${DOCS_ROUTES_DIR}/${page}`);
if (pageText.includes("@/lib/command-reference")) continue;
diff --git a/scripts/gen-command-reference.d.mts b/scripts/gen-command-reference.d.mts
index d7984b4969..990803fb95 100644
--- a/scripts/gen-command-reference.d.mts
+++ b/scripts/gen-command-reference.d.mts
@@ -24,11 +24,13 @@ export declare function renderCommandList(entries: CommandCatalogEntry[]): strin
export declare function collectCommandCatalogs(options?: CommandCatalogOptions): {
publicCommands: CommandCatalogEntry[];
maintainerCommands: CommandCatalogEntry[];
+ actionCommands: CommandCatalogEntry[];
};
export declare function renderCommandReferenceModule(catalogs: {
publicCommands: CommandCatalogEntry[];
maintainerCommands: CommandCatalogEntry[];
+ actionCommands: CommandCatalogEntry[];
}): string;
export declare function writeCommandReference(options?: WriteCommandReferenceOptions): {
@@ -36,4 +38,5 @@ export declare function writeCommandReference(options?: WriteCommandReferenceOpt
outputPath: string;
publicCommands: CommandCatalogEntry[];
maintainerCommands: CommandCatalogEntry[];
+ actionCommands: CommandCatalogEntry[];
};
diff --git a/scripts/gen-command-reference.mjs b/scripts/gen-command-reference.mjs
index 23604f34df..f39b89d0cf 100644
--- a/scripts/gen-command-reference.mjs
+++ b/scripts/gen-command-reference.mjs
@@ -13,6 +13,7 @@ export const DEFAULT_OUTPUT_PATH = "apps/gittensory-ui/src/lib/command-reference
const PUBLIC_CATALOG_NAME = "PUBLIC_MENTION_COMMAND_CATALOG";
const MAINTAINER_CATALOG_NAME = "MAINTAINER_QUEUE_DIGEST_COMMAND_CATALOG";
+const ACTION_CATALOG_NAME = "GITTENSORY_ACTION_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
@@ -47,23 +48,38 @@ export function collectCommandCatalogs({ rootDir = process.cwd(), sourcePath = D
const sourceText = readFileSync(resolve(rootDir, sourcePath), "utf8");
const publicCommands = extractCatalogEntries(sourceText, PUBLIC_CATALOG_NAME);
const maintainerCommands = extractCatalogEntries(sourceText, MAINTAINER_CATALOG_NAME);
+ const actionCommands = extractCatalogEntries(sourceText, ACTION_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 };
+ if (actionCommands.length < 7) {
+ throw new Error(
+ `gen-command-reference: extraction found only ${actionCommands.length} PR action command(s) in ${sourcePath} -- expected 7; the extraction regex may be broken`,
+ );
+ }
+ return { publicCommands, maintainerCommands, actionCommands };
}
-export function renderCommandReferenceModule({ publicCommands, maintainerCommands }) {
+export function renderCommandReferenceModule({ publicCommands, maintainerCommands, actionCommands }) {
return `// Generated by scripts/gen-command-reference.mjs. Do not edit manually.
// Regenerate via \`npm run command-reference\`.
+export const PUBLIC_COMMAND_ENTRIES = ${JSON.stringify(publicCommands, null, 2)} as const;
+
export const PUBLIC_COMMAND_LIST =
${JSON.stringify(renderCommandList(publicCommands))};
+export const MAINTAINER_COMMAND_ENTRIES = ${JSON.stringify(maintainerCommands, null, 2)} as const;
+
export const MAINTAINER_COMMAND_LIST =
${JSON.stringify(renderCommandList(maintainerCommands))};
+
+export const ACTION_COMMAND_ENTRIES = ${JSON.stringify(actionCommands, null, 2)} as const;
+
+export const ACTION_COMMAND_LIST =
+ ${JSON.stringify(renderCommandList(actionCommands))};
`;
}
@@ -73,8 +89,8 @@ export function writeCommandReference({
outputPath = DEFAULT_OUTPUT_PATH,
check = false,
} = {}) {
- const { publicCommands, maintainerCommands } = collectCommandCatalogs({ rootDir, sourcePath });
- const output = renderCommandReferenceModule({ publicCommands, maintainerCommands });
+ const { publicCommands, maintainerCommands, actionCommands } = collectCommandCatalogs({ rootDir, sourcePath });
+ const output = renderCommandReferenceModule({ publicCommands, maintainerCommands, actionCommands });
const absOutput = resolve(rootDir, outputPath);
const current = existsSync(absOutput) ? readFileSync(absOutput, "utf8") : null;
const changed = current !== output;
@@ -82,7 +98,7 @@ export function writeCommandReference({
mkdirSync(dirname(absOutput), { recursive: true });
writeFileSync(absOutput, output);
}
- return { changed, outputPath, publicCommands, maintainerCommands };
+ return { changed, outputPath, publicCommands, maintainerCommands, actionCommands };
}
function main(argv) {
@@ -93,7 +109,7 @@ function main(argv) {
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`,
+ `gen-command-reference: ${check ? "checked" : "wrote"} ${result.publicCommands.length} public + ${result.maintainerCommands.length} maintainer-only + ${result.actionCommands.length} action command references in ${result.outputPath}\n`,
);
}
diff --git a/src/github/commands.ts b/src/github/commands.ts
index e8c4bcff7f..5888db0d22 100644
--- a/src/github/commands.ts
+++ b/src/github/commands.ts
@@ -4,7 +4,7 @@ import {
suggestCommand as suggestCommandFromCatalog,
type CommandSuggestCatalog,
} from "./command-suggest";
-import { gittensoryFooter } from "./footer";
+import { gittensoryFooter, GITTENSORY_SITE_URL } from "./footer";
import type { AgentRunBundle } from "../services/agent-orchestrator";
import type { GittensorContributorSnapshot, OfficialGittensorMinerDetection } from "../gittensor/api";
import type { AgentActionRecord, RepositoryCommandAuthorizationPolicy } from "../types";
@@ -780,6 +780,8 @@ function helpSections(unknownVerb?: string | undefined): string[] {
"- `@gittensory noise-report` highlights queue noise to triage first.",
"",
...actionCommandHelpSections(),
+ "",
+ `- Full command reference (syntax, roles, gate boundary): ${GITTENSORY_SITE_URL}/docs/gittensory-commands`,
];
}
diff --git a/test/unit/gen-command-reference-script.test.ts b/test/unit/gen-command-reference-script.test.ts
index af8b1717bb..216100fe35 100644
--- a/test/unit/gen-command-reference-script.test.ts
+++ b/test/unit/gen-command-reference-script.test.ts
@@ -121,16 +121,19 @@ describe("gen-command-reference script (#3046)", () => {
});
describe("renderCommandReferenceModule", () => {
- it("renders a generated-file header plus both exported string constants", () => {
+ it("renders a generated-file header plus 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." }],
+ actionCommands: [{ id: "review", title: "Review", description: "Request review." }],
});
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";');
+ expect(output).toContain('export const ACTION_COMMAND_LIST =\n "@gittensory review";');
+ expect(output).toContain("export const ACTION_COMMAND_ENTRIES");
});
});
@@ -141,8 +144,8 @@ describe("gen-command-reference script (#3046)", () => {
// 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 { publicCommands, maintainerCommands, actionCommands } = collectCommandCatalogs({ rootDir });
+ const expected = renderCommandReferenceModule({ publicCommands, maintainerCommands, actionCommands });
const actual = readFileSync(resolve(rootDir, DEFAULT_OUTPUT_PATH), "utf8");
@@ -152,6 +155,6 @@ describe("gen-command-reference script (#3046)", () => {
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/);
+ expect(output).toMatch(/gen-command-reference: checked \d+ public \+ \d+ maintainer-only \+ \d+ action command references in/);
});
});
diff --git a/test/unit/github-commands.test.ts b/test/unit/github-commands.test.ts
index e30cbf3f82..c2c059ecab 100644
--- a/test/unit/github-commands.test.ts
+++ b/test/unit/github-commands.test.ts
@@ -174,6 +174,12 @@ describe("GitHub mention commands", () => {
expect(helpCard).toContain("Additional safe details");
});
+ it("helpSections links to the public command reference doc (#2171)", () => {
+ const body = githubCommandsInternals.helpSections().join("\n");
+ expect(body).toContain("https://gittensory.aethereal.dev/docs/gittensory-commands");
+ expect(body).toContain("Full command reference");
+ });
+
it("isGittensoryActionCommand distinguishes action verbs from Q&A commands", () => {
for (const action of ["gate-override", "review", "pause", "resume", "resolve", "configuration", "explain"] as const) {
expect(isGittensoryActionCommand(action)).toBe(true);