refactor: tier sf slash commands

This commit is contained in:
Mikael Hugo 2026-05-14 20:14:09 +02:00
parent 587b5fa31c
commit ab1a1edcf9
8 changed files with 252 additions and 154 deletions

View file

@ -1161,6 +1161,7 @@ export type MessageRenderer<T = unknown> = (
export interface RegisteredCommand {
name: string;
description?: string;
menuTier?: "primary" | "secondary" | "internal";
getArgumentCompletions?: (
argumentPrefix: string,
) => AutocompleteItem[] | null;

View file

@ -118,6 +118,8 @@ import { UserMessageComponent } from "./components/user-message.js";
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
import { handleAgentEvent } from "./controllers/chat-controller.js";
import { createExtensionUIContext as buildExtensionUIContext } from "./controllers/extension-ui-controller.js";
import { setupEditorSubmitHandler as setupEditorSubmitHandlerController } from "./controllers/input-controller.js";
import { updateAvailableProviderCount as updateAvailableProviderCountController } from "./controllers/model-controller.js";
import {
buildScopeGroups,
formatDiagnostics,
@ -125,8 +127,6 @@ import {
formatScopeGroups,
getShortPath,
} from "./resource-display.js";
import { setupEditorSubmitHandler as setupEditorSubmitHandlerController } from "./controllers/input-controller.js";
import { updateAvailableProviderCount as updateAvailableProviderCountController } from "./controllers/model-controller.js";
import {
getAppKeyDisplay,
type SlashCommandContext,
@ -603,6 +603,7 @@ export class InteractiveMode {
).map((cmd) => ({
name: cmd.name,
description: cmd.description ?? "(extension command)",
menuTier: cmd.menuTier,
getArgumentCompletions: cmd.getArgumentCompletions,
}));
@ -1110,10 +1111,7 @@ export class InteractiveMode {
);
if (collisionDiags.length > 0) {
const collisionLines = formatDiagnostics(
collisionDiags,
metadata,
);
const collisionLines = formatDiagnostics(collisionDiags, metadata);
this.chatContainer.addChild(
new Text(
`${theme.fg("warning", "[Skill conflicts]")}\n${collisionLines}`,
@ -1139,10 +1137,7 @@ export class InteractiveMode {
const promptDiagnostics = promptsResult.diagnostics;
if (promptDiagnostics.length > 0) {
const warningLines = formatDiagnostics(
promptDiagnostics,
metadata,
);
const warningLines = formatDiagnostics(promptDiagnostics, metadata);
this.chatContainer.addChild(
new Text(
`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`,
@ -1175,10 +1170,7 @@ export class InteractiveMode {
extensionDiagnostics.push(...shortcutDiagnostics);
if (extensionDiagnostics.length > 0) {
const warningLines = formatDiagnostics(
extensionDiagnostics,
metadata,
);
const warningLines = formatDiagnostics(extensionDiagnostics, metadata);
this.chatContainer.addChild(
new Text(
`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`,

View file

@ -30,6 +30,31 @@ describe("CombinedAutocompleteProvider — slash commands", () => {
assert.equal(result!.prefix, "/");
});
it("returns only primary commands for bare / when tiers are configured", () => {
const provider = makeProvider([
{ name: "help", description: "Help", menuTier: "primary" },
{ name: "next", description: "Step", menuTier: "primary" },
{ name: "queue", description: "Queue", menuTier: "secondary" },
]);
const result = provider.getSuggestions(["/"], 0, 1);
assert.ok(result);
assert.deepEqual(result!.items.map((item) => item.value).sort(), [
"help",
"next",
]);
});
it("searches secondary commands after a prefix is typed", () => {
const provider = makeProvider([
{ name: "quick", description: "Quick", menuTier: "primary" },
{ name: "queue", description: "Queue", menuTier: "secondary" },
]);
const result = provider.getSuggestions(["/q"], 0, 2);
assert.ok(result);
assert.ok(result!.items.some((item) => item.value === "quick"));
assert.ok(result!.items.some((item) => item.value === "queue"));
});
it("filters commands by typed prefix", () => {
const provider = makeProvider(sampleCommands);
const result = provider.getSuggestions(["/se"], 0, 3);

View file

@ -114,6 +114,7 @@ export interface AutocompleteItem {
export interface SlashCommand {
name: string;
description?: string;
menuTier?: "primary" | "secondary" | "internal";
// Function to get argument completions for this command
// Returns null if no argument completion is available
getArgumentCompletions?(argumentPrefix: string): AutocompleteItem[] | null;
@ -207,22 +208,30 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
name: "name" in cmd ? cmd.name : cmd.value,
label: "name" in cmd ? cmd.name : cmd.label,
description: cmd.description,
menuTier: "menuTier" in cmd ? cmd.menuTier : undefined,
}));
const filtered = fuzzyFilter(
commandItems,
prefix,
(item) => item.name,
).map((item) => ({
let filtered = fuzzyFilter(commandItems, prefix, (item) => item.name);
if (prefix.length === 0) {
const primary = filtered.filter(
(item) => item.menuTier === "primary",
);
if (primary.length > 0) {
filtered = primary;
}
}
const items = filtered.map((item) => ({
value: item.name,
label: item.label,
...(item.description && { description: item.description }),
}));
if (filtered.length === 0) return null;
if (items.length === 0) return null;
return {
items: filtered,
items,
prefix: `/${prefix}`,
};
} else {

View file

@ -318,15 +318,14 @@ export const TOP_LEVEL_SUBCOMMANDS = [
];
// Lean product surface (UOK control-plane convo 2026-05-14): operators
// pick modes, not personas, and SF runs implementation machinery (triage,
// agent picks, mode/work-mode shifts, orchestration internals) on its own.
// Commands not in this set stay callable for scripting/debug but don't
// show up in the slash catalog or help.
// pick workflows, not personas, and SF runs implementation machinery (agent
// picks, mode/work-mode shifts, orchestration internals) on its own. Commands
// not in this set stay callable for scripting/debug but don't show up in the
// slash catalog or help.
//
// Hidden by category:
// - /triage, /agent, /parallel, /cmux, /sidekicks — internal machinery
// - /agent, /parallel, /cmux, /sidekicks — internal orchestration machinery
// - /mode, /control, /permission-profile, /model-mode — Shift+Tab + advanced axes
// - /repair — auto-detected work-mode shift, fold into /autonomous
// - /hooks, /run-hook, /skill-health, /inspect, /recover — diagnostics
// - /mcp, /extensions, /configure-agent, /experimental — platform plumbing
// - /delegate, /pr-branch, /add-tests — lower-level conveniences reached
@ -370,6 +369,28 @@ export const PUBLIC_DIRECT_COMMANDS = new Set([
"workflow",
]);
/**
* Primary command tier the 5 things an operator should memorize. Four
* product modes plus /help. Everything else is reachable via autocomplete
* (PUBLIC_DIRECT_COMMANDS) or operator-driven discovery (`/help all`).
*
* Rationale (2026-05-14 product-surface convo): a 34-command menu defeats
* the product-mode framing. Operators interact with modes (`/next`,
* `/discuss`, `/autonomous`) plus the non-workflow lane (`/quick`); SF
* surfaces relevant secondary commands inline when context warrants.
*/
export const PRIMARY_COMMANDS = new Set([
"next",
"autonomous",
"discuss",
"quick",
"help",
]);
export const PRIMARY_TOP_LEVEL_SUBCOMMANDS = TOP_LEVEL_SUBCOMMANDS.filter(
(command) => PRIMARY_COMMANDS.has(command.cmd),
);
export const PUBLIC_TOP_LEVEL_SUBCOMMANDS = TOP_LEVEL_SUBCOMMANDS.filter(
(command) => PUBLIC_DIRECT_COMMANDS.has(command.cmd),
);

View file

@ -25,138 +25,156 @@ import {
formatProgressLine,
} from "../../progress-score.js";
import { setSessionModelOverride } from "../../session-model-override.js";
import { sfHome } from "../../sf-home.js";
import { formattedShortcutPair } from "../../shortcut-defs.js";
import { deriveState } from "../../state.js";
import { writeUokDiagnostics } from "../../uok/diagnostic-synthesis.js";
import { PRIMARY_COMMANDS, PUBLIC_TOP_LEVEL_SUBCOMMANDS } from "../catalog.js";
import { projectRoot } from "../context.js";
import { sfHome } from "../../sf-home.js";
const HELP_CATEGORY_ORDER = [
{
title: "PRIMARY",
commands: ["next", "autonomous", "discuss", "quick", "help"],
},
{
title: "WORKFLOW",
commands: [
"start",
"templates",
"workflow",
"ship",
"backlog",
"schedule",
"triage",
],
},
{
title: "STATUS",
commands: ["status", "queue", "visualize", "history", "logs", "forensics"],
},
{
title: "CONTROL",
commands: ["pause", "stop", "skip", "undo", "park", "unpark", "capture"],
},
{
title: "SETUP",
commands: [
"init",
"setup",
"doctor",
"repair",
"remote",
"knowledge",
"config",
"keys",
"model",
"prefs",
"update",
],
},
];
function commandByName() {
return new Map(
PUBLIC_TOP_LEVEL_SUBCOMMANDS.map((command) => [command.cmd, command]),
);
}
function commandLine(command) {
return ` /${command.cmd.padEnd(12)} ${command.desc ?? ""}`.trimEnd();
}
function groupedPublicHelpLines() {
const byName = commandByName();
const emitted = new Set();
const lines = ["SF — Singularity Forge workflow runtime\n"];
for (const category of HELP_CATEGORY_ORDER) {
const categoryLines = category.commands
.map((name) => byName.get(name))
.filter(Boolean)
.map((command) => {
emitted.add(command.cmd);
return commandLine(command);
});
if (categoryLines.length === 0) continue;
lines.push(category.title, ...categoryLines, "");
}
const other = PUBLIC_TOP_LEVEL_SUBCOMMANDS.filter(
(command) => !emitted.has(command.cmd),
);
if (other.length > 0) {
lines.push("OTHER", ...other.map(commandLine), "");
}
return lines;
}
function keywordHelpLines(keyword) {
const needle = keyword.toLowerCase();
const tokens = needle
.split(/[^a-z0-9-]+/i)
.map((token) => token.trim())
.filter(
(token) =>
token.length > 2 &&
!new Set([
"the",
"and",
"for",
"with",
"want",
"show",
"see",
"my",
"how",
"what",
"where",
]).has(token),
);
const matches = PUBLIC_TOP_LEVEL_SUBCOMMANDS.filter(
(command) =>
command.cmd.toLowerCase().includes(needle) ||
(command.desc ?? "").toLowerCase().includes(needle) ||
tokens.some(
(token) =>
command.cmd.toLowerCase().includes(token) ||
(command.desc ?? "").toLowerCase().includes(token),
),
);
if (matches.length === 0) {
return [
`No public SF command matched "${keyword}".`,
"Try /help all, or describe the work in /discuss.",
];
}
return [`SF command matches for "${keyword}"\n`, ...matches.map(commandLine)];
}
export function showHelp(ctx, args = "") {
const summaryLines = [
"SF — Singularity Forge\n",
"QUICK START",
" /start <tpl> Start a workflow template",
" /next Run one assisted unit",
" /autonomous Run all queued product units continuously",
" /pause Pause autonomous mode",
" /autonomous stop Stop autonomous mode gracefully",
"",
"VISIBILITY",
` /status Dashboard (${formattedShortcutPair("dashboard")})`,
` /parallel watch Parallel monitor (${formattedShortcutPair("parallel")})`,
` /notifications Notification history (${formattedShortcutPair("notifications")})`,
" /tasks Background work surface — units, workers, budget",
" /visualize Interactive 10-tab TUI",
" /queue Show queued/dispatched units",
" /research Force research stage",
" /plan Force planning stage",
" /implement Force implementation stage",
"",
"COURSE CORRECTION",
" /steer <desc> Apply user override to active work",
" /steer mode <m> [scope] Change work mode (now|after-current-unit|next-milestone)",
" /steer permission-profile <p> [scope] Change permission profile",
" /steer model-mode <m> Change model mode for next unit",
" /capture <text> Quick-capture a thought to CAPTURES.md",
" /triage Classify and route pending captures",
" /undo Revert last completed unit [--force]",
" /rethink Conversational project reorganization",
"",
"SETUP",
" /init Project init wizard",
" /setup Global setup status [llm|search|remote|keys|prefs]",
" /reload Snapshot and reload agent with fresh extension code",
" /model Switch active session model",
" /prefs Manage preferences",
" /doctor Diagnose and repair .sf/ state",
" /repair Switch to repair work mode and run diagnostics",
" /tasks Background work surface",
" /skills List discovered skills [reload|--eval <name>|--auto-create]",
" /cost Show cost summary [--session|--all|--prometheus]",
"",
"Use /help all for the complete command reference.",
];
const allLines = [
"SF — Singularity Forge\n",
"WORKFLOW",
" /start <tpl> Start a workflow template (bugfix, spike, feature, hotfix, etc.)",
" /templates List available workflow templates [info <name>]",
" /next Run one assisted unit",
" /next Assisted mode: execute next task, then pause [--dry-run] [--verbose]",
" /autonomous Run all queued product units continuously [--verbose]",
" /autonomous stop Stop autonomous mode gracefully",
" /pause Pause autonomous mode (preserves state, /autonomous to resume)",
" /discuss Start guided milestone/slice discussion",
" /new-milestone Create milestone from headless context (used by sf headless)",
"",
"VISIBILITY",
` /status Show progress dashboard (${formattedShortcutPair("dashboard")})`,
` /parallel watch Open parallel worker monitor (${formattedShortcutPair("parallel")})`,
" /visualize Interactive 10-tab TUI (progress, timeline, deps, metrics, health, agent, changes, knowledge, captures, export)",
" /queue Show queued/dispatched units and execution order",
" /tasks Background work surface — units, workers, budget, checkpoints",
" /research Force research stage for current unit",
" /plan Force planning stage for current unit",
" /implement Force implementation stage for current unit",
" /history View execution history [--cost] [--phase] [--model] [N]",
" /trajectory View execution trajectory — step-by-step trace with costs and errors",
" /changelog Show categorized release notes [version]",
` /notifications View persistent notification history [clear|tail|filter] (${formattedShortcutPair("notifications")})`,
"",
"COURSE CORRECTION",
" /steer <desc> Apply user override to active work",
" /capture <text> Quick-capture a thought to CAPTURES.md",
" /triage Classify and route pending captures",
" /skip <unit> Prevent a unit from autonomous mode dispatch",
" /undo Revert last completed unit [--force]",
" /rethink Conversational project reorganization — reorder, park, discard, add milestones",
" /park [id] Park a milestone — skip without deleting [reason]",
" /unpark [id] Reactivate a parked milestone",
"",
"PROJECT KNOWLEDGE",
" /knowledge <type> <text> Add rule, pattern, or lesson to KNOWLEDGE.md",
" /codebase [generate|update|stats|indexer] Manage CODEBASE.md and Sift code search",
"",
"SCHEDULE",
" /schedule add --in <dur> <title> Schedule a follow-up item",
" /schedule list Show pending scheduled items",
" /schedule done <id> Mark an item complete",
"",
"SETUP & CONFIGURATION",
" /init Project init wizard — detect, configure, bootstrap .sf/",
" /setup Global setup status [llm|search|remote|keys|prefs]",
" /model Switch active session model [provider/model|model-id]",
" /mode Switch work mode (chat/plan/build/review/repair/research)",
" /control Switch run control (manual/assisted/autonomous)",
" /permission-profile Switch permission profile (restricted/normal/trusted/unrestricted)",
" /model-mode Switch model mode (fast/smart/deep)",
" /prefs Manage preferences [global|project|status|wizard|setup|import-claude]",
" /cmux Manage cmux integration [status|on|off|notifications|sidebar|splits|browser]",
" /config Set API keys for external tools",
" /keys API key manager [list|add|remove|test|rotate|doctor]",
" /show-config Show effective configuration (models, routing, toggles)",
" /hooks Show post-unit hook configuration",
" /extensions Manage extensions [list|enable|disable|info]",
" /fast Toggle OpenAI service tier [on|off|flex|status]",
" /mcp External MCP server status [status|check <server>|reload]",
"",
"MAINTENANCE",
" /doctor Diagnose and repair .sf/ state [audit|fix|heal] [scope]",
" /repair Switch to repair work mode and run diagnostics [--autonomous]",
" /tasks Background work surface [--refresh|--failed|--cancelled|--all]",
" /skills List discovered skills from .agents/skills/",
" /skills reload Reload skills from disk — picks up new/updated skill files",
" /skills --eval <name> Run eval cases for a skill",
" /reload Snapshot & reload agent, resume same session",
" /export Export milestone/slice results [--json|--markdown|--html] [--all]",
" /cleanup Remove merged branches or snapshots [branches|snapshots]",
" /worktree Manage worktrees from the TUI [list|merge|clean|remove]",
" /migrate Migrate .planning/ (v1) to .sf/ (v2) format",
" /remote Configure remote question delivery [slack|discord|status|disconnect]",
" /inspect Show SQLite DB diagnostics (schema, row counts, recent entries)",
" /update Update SF to the latest version via npm",
];
const showAll = args.trim().toLowerCase() === "all";
ctx.ui.notify((showAll ? allLines : summaryLines).join("\n"), "info");
const request = args.trim();
if (request.length > 0 && request.toLowerCase() !== "all") {
ctx.ui.notify(keywordHelpLines(request).join("\n"), "info");
return;
}
if (request.toLowerCase() === "all") {
ctx.ui.notify(groupedPublicHelpLines().join("\n"), "info");
return;
}
const primary = PUBLIC_TOP_LEVEL_SUBCOMMANDS.filter((command) =>
PRIMARY_COMMANDS.has(command.cmd),
);
ctx.ui.notify(
[
"SF — Singularity Forge workflow runtime\n",
"PRIMARY",
...primary.map(commandLine),
"",
`Shift+Tab cycles runtime mode. ${formattedShortcutPair("dashboard")} opens /status.`,
"Type /<letters> to search secondary commands.",
"Use /help all for the grouped public command reference.",
].join("\n"),
"info",
);
}
export async function handleStatus(ctx) {
const basePath = projectRoot();

View file

@ -2,6 +2,7 @@ import { importExtensionModule } from "@singularity-forge/coding-agent";
import {
DIRECT_SF_COMMANDS,
getSfTopLevelCommandCompletions,
PRIMARY_COMMANDS,
SF_COMMAND_DESCRIPTION,
} from "./catalog.js";
@ -30,6 +31,7 @@ export function registerSFCommands(pi) {
for (const command of DIRECT_SF_COMMANDS) {
pi.registerCommand(command.cmd, {
description: command.desc || SF_COMMAND_DESCRIPTION,
menuTier: PRIMARY_COMMANDS.has(command.cmd) ? "primary" : "secondary",
getArgumentCompletions: (prefix) =>
getSfTopLevelCommandCompletions(command.cmd, prefix),
handler: async (args, ctx) => {

View file

@ -7,7 +7,9 @@ import {
DIRECT_SF_COMMAND_NAMES,
getSfArgumentCompletions,
getSfTopLevelCommandCompletions,
PRIMARY_COMMANDS,
} from "../commands/catalog.js";
import { showHelp } from "../commands/handlers/core.js";
import { registerSFCommands } from "../commands/index.js";
test("direct SF command surface registers workflow verbs without legacy sf namespace", () => {
@ -44,6 +46,34 @@ test("top_level_completions_keep_platform_owned_product_paths_visible", () => {
assert.equal(labels.includes("permission-profile"), false);
});
test("primary_command_tier_is_the_five_command_memory_model", () => {
assert.deepEqual([...PRIMARY_COMMANDS].sort(), [
"autonomous",
"discuss",
"help",
"next",
"quick",
]);
});
test("help_keyword_routes_natural_language_to_public_commands", () => {
const messages = [];
showHelp(
{
ui: {
notify(message) {
messages.push(message);
},
},
},
"I want to see my queue",
);
assert.equal(messages.length, 1);
assert.match(messages[0], /\/queue\b/);
assert.doesNotMatch(messages[0], /\/parallel\b/);
});
test("direct command completions strip the already typed command name", () => {
assert.deepEqual(getSfTopLevelCommandCompletions("autonomous", "--"), [
{