refactor: narrow sf slash surface

This commit is contained in:
Mikael Hugo 2026-05-14 20:04:53 +02:00
parent 5ce9df2e37
commit 587b5fa31c
5 changed files with 117 additions and 106 deletions

View file

@ -176,6 +176,22 @@ function parseTopLevelCatalogCommands() {
); );
} }
function parsePublicDirectCommands() {
const source = read(join(sfRoot, "commands", "catalog.js"));
const start = source.indexOf("export const PUBLIC_DIRECT_COMMANDS");
const end = source.indexOf("export const PUBLIC_TOP_LEVEL_SUBCOMMANDS");
if (start === -1 || end === -1 || end <= start) {
throw new Error(
"Could not locate PUBLIC_DIRECT_COMMANDS in commands/catalog.js",
);
}
return new Set(
[...source.slice(start, end).matchAll(/"([^"]+)"/g)].map(
(match) => match[1],
),
);
}
function parseHandledTopLevelCommands() { function parseHandledTopLevelCommands() {
const handlerFiles = [ const handlerFiles = [
"core.js", "core.js",
@ -268,10 +284,13 @@ function main() {
const manifest = parseManifest(); const manifest = parseManifest();
const registeredTools = parseRegisteredTools(); const registeredTools = parseRegisteredTools();
const catalogCommands = parseTopLevelCatalogCommands(); const catalogCommands = parseTopLevelCatalogCommands();
const publicDirectCommands = parsePublicDirectCommands();
const directCommandNames = uniqueSorted( const directCommandNames = uniqueSorted(
BASE_DIRECT_COMMAND_NAMES.concat( BASE_DIRECT_COMMAND_NAMES.concat(
catalogCommands.filter( catalogCommands.filter(
(command) => !BASE_RUNTIME_COMMAND_NAMES.has(command), (command) =>
publicDirectCommands.has(command) &&
!BASE_RUNTIME_COMMAND_NAMES.has(command),
), ),
parseDirectRegisteredCommands().filter( parseDirectRegisteredCommands().filter(
(command) => !BASE_RUNTIME_COMMAND_NAMES.has(command), (command) => !BASE_RUNTIME_COMMAND_NAMES.has(command),

View file

@ -19,55 +19,31 @@ const TOP_LEVEL_SUBCOMMANDS = [
{ cmd: "quick", desc: "Execute a quick task without full planning overhead" }, { cmd: "quick", desc: "Execute a quick task without full planning overhead" },
{ cmd: "discuss", desc: "Discuss architecture and decisions" }, { cmd: "discuss", desc: "Discuss architecture and decisions" },
{ cmd: "capture", desc: "Fire-and-forget thought capture" }, { cmd: "capture", desc: "Fire-and-forget thought capture" },
{ cmd: "changelog", desc: "Show categorized release notes" },
{ cmd: "triage", desc: "Manually trigger triage of pending captures" }, { cmd: "triage", desc: "Manually trigger triage of pending captures" },
{ cmd: "dispatch", desc: "Dispatch a specific phase directly" },
{ cmd: "history", desc: "View execution history" }, { cmd: "history", desc: "View execution history" },
{ cmd: "undo", desc: "Revert last completed unit" }, { cmd: "undo", desc: "Revert last completed unit" },
{ cmd: "skip", desc: "Prevent a unit from autonomous dispatch" }, { cmd: "skip", desc: "Prevent a unit from autonomous dispatch" },
{ cmd: "export", desc: "Export milestone or slice results" }, { cmd: "park", desc: "Park a milestone" },
{ cmd: "cleanup", desc: "Remove merged branches or snapshots" }, { cmd: "unpark", desc: "Reactivate a parked milestone" },
{ cmd: "mode", desc: "Switch workflow mode (solo/team)" },
{ cmd: "prefs", desc: "Manage preferences" }, { cmd: "prefs", desc: "Manage preferences" },
{ cmd: "config", desc: "Set API keys for external tools" }, { cmd: "config", desc: "Set API keys for external tools" },
{ cmd: "keys", desc: "API key manager" }, { cmd: "keys", desc: "API key manager" },
{ cmd: "hooks", desc: "Show configured hooks" }, { cmd: "model", desc: "Switch the active session model or open a picker" },
{ cmd: "run-hook", desc: "Manually trigger a specific hook" },
{ cmd: "skill-health", desc: "Skill lifecycle dashboard" },
{ cmd: "doctor", desc: "Runtime health checks with auto-fix" }, { cmd: "doctor", desc: "Runtime health checks with auto-fix" },
{ cmd: "uok", desc: "UOK runtime health, ledger status, and gate metrics" }, { cmd: "repair", desc: "Repair runtime state and generated artifacts" },
{ cmd: "logs", desc: "Browse activity logs, debug logs, and metrics" }, { cmd: "logs", desc: "Browse activity logs, debug logs, and metrics" },
{ cmd: "forensics", desc: "Examine execution logs" }, { cmd: "forensics", desc: "Examine execution logs" },
{ cmd: "init", desc: "Project init wizard" }, { cmd: "init", desc: "Project init wizard" },
{ cmd: "setup", desc: "Global setup status and configuration" }, { cmd: "setup", desc: "Global setup status and configuration" },
{ cmd: "migrate", desc: "Migrate a v1 .planning directory to .sf format" },
{ cmd: "remote", desc: "Configure remote question delivery" }, { cmd: "remote", desc: "Configure remote question delivery" },
{ cmd: "steer", desc: "Hard-steer plan documents during execution" },
{ cmd: "inspect", desc: "Show SQLite DB diagnostics" },
{ cmd: "knowledge", desc: "Add persistent project knowledge" }, { cmd: "knowledge", desc: "Add persistent project knowledge" },
{
cmd: "new-milestone",
desc: "Create a milestone from a specification document",
},
{ cmd: "parallel", desc: "Parallel milestone orchestration" },
{ cmd: "park", desc: "Park a milestone" },
{ cmd: "unpark", desc: "Reactivate a parked milestone" },
{ cmd: "update", desc: "Update SF to the latest version" }, { cmd: "update", desc: "Update SF to the latest version" },
{ cmd: "start", desc: "Start a workflow template" }, { cmd: "start", desc: "Start a workflow template" },
{ cmd: "templates", desc: "List available workflow templates" }, { cmd: "templates", desc: "List available workflow templates" },
{ cmd: "extensions", desc: "Manage extensions" }, { cmd: "workflow", desc: "Inspect or continue workflow state" },
{ { cmd: "ship", desc: "Prepare changes for review and delivery" },
cmd: "codebase", { cmd: "schedule", desc: "Manage time-bound reminders" },
desc: "Generate, refresh, and inspect the codebase map cache", { cmd: "backlog", desc: "Inspect priority-ordered work" },
},
{
cmd: "solver-eval",
desc: "Compare raw agent loops against SF autonomous solver control",
},
{
cmd: "scaffold",
desc: "Inspect or refresh ADR-021 versioned scaffold docs",
},
]; ];
function filterStartsWith(partial, options, prefix = "") { function filterStartsWith(partial, options, prefix = "") {
const normalizedPrefix = prefix.length > 0 ? `${prefix} ` : ""; const normalizedPrefix = prefix.length > 0 ? `${prefix} ` : "";

View file

@ -11,7 +11,7 @@ import { resolveProjectRoot } from "../worktree.js";
* Comprehensive description of all available SF commands for help text. * Comprehensive description of all available SF commands for help text.
*/ */
export const SF_COMMAND_DESCRIPTION = export const SF_COMMAND_DESCRIPTION =
"SF — Singularity Forge: /help|start|templates|next|autonomous|pause|status|widget|visualize|queue|quick|discuss|capture|triage|todo|dispatch|history|undo|undo-task|reset-slice|rate|skip|cleanup|mode|control|permission-profile|model-mode|show-config|prefs|config|keys|hooks|run-hook|skill-health|doctor|uok|logs|forensics|migrate|remote|steer|knowledge|harness|solver-eval|new-milestone|parallel|cmux|park|unpark|init|setup|inspect|extensions|update|fast|mcp|rethink|codebase|notifications|ship|do|session-report|backlog|pr-branch|add-tests|scan|scaffold|extract-learnings|eval-review|plan|agent"; "SF — Singularity Forge workflow runtime: /next for assisted build, /discuss for plan/discovery, /autonomous for the full research-plan-execute loop.";
export const BASE_RUNTIME_COMMANDS = new Set([ export const BASE_RUNTIME_COMMANDS = new Set([
"settings", "settings",
@ -117,7 +117,7 @@ export const TOP_LEVEL_SUBCOMMANDS = [
{ cmd: "model", desc: "Switch the active session model or open a picker" }, { cmd: "model", desc: "Switch the active session model or open a picker" },
{ {
cmd: "mode", cmd: "mode",
desc: "Switch mode: ask · plan · build · yolo (full autonomy) — or Shift+Tab to cycle", desc: "Internal mode compatibility; use /discuss, /next, or /autonomous for product workflows",
}, },
{ {
cmd: "control", cmd: "control",
@ -317,8 +317,67 @@ 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.
//
// Hidden by category:
// - /triage, /agent, /parallel, /cmux, /sidekicks — internal 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
// through /ship, /next, or autonomous workflow
export const PUBLIC_DIRECT_COMMANDS = new Set([
"autonomous",
"backlog",
"capture",
"config",
"discuss",
"doctor",
"forensics",
"help",
"history",
"init",
"keys",
"knowledge",
"logs",
"model",
"next",
"park",
"pause",
"prefs",
"queue",
"quick",
"remote",
"repair",
"schedule",
"ship",
"skip",
"setup",
"start",
"status",
"stop",
"templates",
"triage",
"undo",
"unpark",
"update",
"visualize",
"workflow",
]);
export const PUBLIC_TOP_LEVEL_SUBCOMMANDS = TOP_LEVEL_SUBCOMMANDS.filter(
(command) => PUBLIC_DIRECT_COMMANDS.has(command.cmd),
);
export const DIRECT_SF_COMMANDS = TOP_LEVEL_SUBCOMMANDS.filter( export const DIRECT_SF_COMMANDS = TOP_LEVEL_SUBCOMMANDS.filter(
(command) => !BASE_RUNTIME_COMMANDS.has(command.cmd), (command) =>
PUBLIC_DIRECT_COMMANDS.has(command.cmd) &&
!BASE_RUNTIME_COMMANDS.has(command.cmd),
); );
export const DIRECT_SF_COMMAND_NAMES = DIRECT_SF_COMMANDS.map( export const DIRECT_SF_COMMAND_NAMES = DIRECT_SF_COMMANDS.map(
@ -653,7 +712,7 @@ export function getSfArgumentCompletions(prefix) {
parts.push(""); parts.push("");
} }
if (parts.length <= 1) { if (parts.length <= 1) {
return filterOptions(parts[0] ?? "", TOP_LEVEL_SUBCOMMANDS); return filterOptions(parts[0] ?? "", PUBLIC_TOP_LEVEL_SUBCOMMANDS);
} }
const [command, subcommand = "", third = ""] = parts; const [command, subcommand = "", third = ""] = parts;
if (command === "cmux") { if (command === "cmux") {

View file

@ -56,130 +56,65 @@
"chapter_close" "chapter_close"
], ],
"commands": [ "commands": [
"add-tests",
"ask",
"audit", "audit",
"autonomous", "autonomous",
"backlog", "backlog",
"capture", "capture",
"chronicle",
"cleanup",
"cmux",
"codebase",
"color", "color",
"color-char", "color-char",
"color-config", "color-config",
"color-next", "color-next",
"color-set", "color-set",
"config", "config",
"configure-agent",
"control",
"cost",
"create-extension", "create-extension",
"create-slash-command", "create-slash-command",
"debug",
"delegate",
"diff",
"discuss", "discuss",
"dispatch",
"do",
"doctor", "doctor",
"emoji", "emoji",
"emoji-config", "emoji-config",
"emoji-history", "emoji-history",
"emoji-set", "emoji-set",
"escalate",
"eval-review",
"experimental",
"extensions",
"extract-learnings",
"fast",
"find",
"forensics", "forensics",
"guard-status", "guard-status",
"guard-toggle", "guard-toggle",
"harness",
"help", "help",
"history", "history",
"hooks",
"implement",
"init", "init",
"inspect",
"instructions",
"keep-alive",
"keys", "keys",
"kill", "kill",
"knowledge", "knowledge",
"logs", "logs",
"mcp",
"migrate",
"mode",
"model-mode",
"new-milestone",
"next", "next",
"notifications",
"notify-beep", "notify-beep",
"notify-focus", "notify-focus",
"notify-save-global", "notify-save-global",
"notify-say", "notify-say",
"notify-status", "notify-status",
"notify-threshold", "notify-threshold",
"parallel",
"park", "park",
"pause", "pause",
"permission", "permission",
"permission-mode", "permission-mode",
"permission-profile",
"plan",
"pr-branch",
"prefs", "prefs",
"queue", "queue",
"quick", "quick",
"rate",
"recover",
"remote", "remote",
"rename",
"repair", "repair",
"research",
"reset-slice",
"rethink",
"rewind",
"run-hook",
"scaffold",
"scan",
"schedule", "schedule",
"search",
"session-report",
"setup", "setup",
"ship", "ship",
"show-config",
"sidekicks",
"skill-health",
"skills",
"skip", "skip",
"solver-eval",
"start", "start",
"status", "status",
"statusline",
"steer",
"streamer-mode",
"subagent", "subagent",
"tasks",
"templates", "templates",
"theme",
"todo",
"trajectory",
"triage", "triage",
"undo", "undo",
"undo-task",
"unpark", "unpark",
"uok",
"update", "update",
"usage", "usage",
"visualize", "visualize",
"widget",
"workflow", "workflow",
"worktree",
"wt" "wt"
], ],
"hooks": [ "hooks": [
@ -201,6 +136,11 @@
"turn_start", "turn_start",
"turn_end" "turn_end"
], ],
"shortcuts": ["Ctrl+Alt+G", "Ctrl+Alt+H", "Ctrl+Alt+M", "Ctrl+Shift+H"] "shortcuts": [
"Ctrl+Alt+G",
"Ctrl+Alt+H",
"Ctrl+Alt+M",
"Ctrl+Shift+H"
]
} }
} }

View file

@ -5,6 +5,7 @@ import { test } from "vitest";
import guardrails from "../../guardrails/index.js"; import guardrails from "../../guardrails/index.js";
import { import {
DIRECT_SF_COMMAND_NAMES, DIRECT_SF_COMMAND_NAMES,
getSfArgumentCompletions,
getSfTopLevelCommandCompletions, getSfTopLevelCommandCompletions,
} from "../commands/catalog.js"; } from "../commands/catalog.js";
import { registerSFCommands } from "../commands/index.js"; import { registerSFCommands } from "../commands/index.js";
@ -21,15 +22,28 @@ test("direct SF command surface registers workflow verbs without legacy sf names
const names = registered.map((entry) => entry.name).sort(); const names = registered.map((entry) => entry.name).sort();
assert.ok(names.includes("autonomous")); assert.ok(names.includes("autonomous"));
assert.ok(names.includes("plan")); assert.ok(names.includes("next"));
assert.ok(names.includes("discuss"));
assert.ok(names.includes("schedule")); assert.ok(names.includes("schedule"));
assert.ok(names.includes("doctor")); assert.ok(names.includes("doctor"));
assert.ok(names.includes("status")); assert.ok(names.includes("status"));
assert.ok(names.includes("ship"));
assert.equal(names.includes("plan"), false);
assert.equal(names.includes("model"), false);
assert.equal(names.includes("permission-profile"), false);
assert.ok(!names.includes("sf")); assert.ok(!names.includes("sf"));
assert.ok(!names.includes("stop")); assert.ok(!names.includes("stop"));
assert.deepEqual(names, [...DIRECT_SF_COMMAND_NAMES].sort()); assert.deepEqual(names, [...DIRECT_SF_COMMAND_NAMES].sort());
}); });
test("top_level_completions_keep_platform_owned_product_paths_visible", () => {
const labels = getSfArgumentCompletions("").map((entry) => entry.label);
assert.ok(labels.includes("model"));
assert.equal(labels.includes("mode"), false);
assert.equal(labels.includes("permission-profile"), false);
});
test("direct command completions strip the already typed command name", () => { test("direct command completions strip the already typed command name", () => {
assert.deepEqual(getSfTopLevelCommandCompletions("autonomous", "--"), [ assert.deepEqual(getSfTopLevelCommandCompletions("autonomous", "--"), [
{ {
@ -45,7 +59,7 @@ test("direct command completions strip the already typed command name", () => {
]); ]);
}); });
test("extension_manifest_uses_permission_profile_command_name", () => { test("extension_manifest_hides_advanced_control_axes_from_public_sf_surface", () => {
const manifest = JSON.parse( const manifest = JSON.parse(
readFileSync( readFileSync(
join( join(
@ -56,7 +70,10 @@ test("extension_manifest_uses_permission_profile_command_name", () => {
), ),
); );
const commands = manifest.provides?.commands ?? []; const commands = manifest.provides?.commands ?? [];
assert.equal(commands.includes("permission-profile"), true); assert.equal(commands.includes("permission-profile"), false);
assert.equal(commands.includes("mode"), false);
assert.equal(commands.includes("control"), false);
assert.equal(commands.includes("model-mode"), false);
assert.equal(commands.includes("trust"), false); assert.equal(commands.includes("trust"), false);
}); });