From 587b5fa31cfa7ec0cb1000aab2b68891eca18dbf Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Thu, 14 May 2026 20:04:53 +0200 Subject: [PATCH] refactor: narrow sf slash surface --- scripts/check-sf-extension-inventory.mjs | 21 +++++- .../extensions/sf/commands-bootstrap.js | 40 +++-------- .../extensions/sf/commands/catalog.js | 67 +++++++++++++++-- .../extensions/sf/extension-manifest.json | 72 ++----------------- .../sf/tests/direct-command-surface.test.mjs | 23 +++++- 5 files changed, 117 insertions(+), 106 deletions(-) diff --git a/scripts/check-sf-extension-inventory.mjs b/scripts/check-sf-extension-inventory.mjs index 0b7001114..2e1ae298b 100644 --- a/scripts/check-sf-extension-inventory.mjs +++ b/scripts/check-sf-extension-inventory.mjs @@ -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() { const handlerFiles = [ "core.js", @@ -268,10 +284,13 @@ function main() { const manifest = parseManifest(); const registeredTools = parseRegisteredTools(); const catalogCommands = parseTopLevelCatalogCommands(); + const publicDirectCommands = parsePublicDirectCommands(); const directCommandNames = uniqueSorted( BASE_DIRECT_COMMAND_NAMES.concat( catalogCommands.filter( - (command) => !BASE_RUNTIME_COMMAND_NAMES.has(command), + (command) => + publicDirectCommands.has(command) && + !BASE_RUNTIME_COMMAND_NAMES.has(command), ), parseDirectRegisteredCommands().filter( (command) => !BASE_RUNTIME_COMMAND_NAMES.has(command), diff --git a/src/resources/extensions/sf/commands-bootstrap.js b/src/resources/extensions/sf/commands-bootstrap.js index d6a1322d6..eb7340da8 100644 --- a/src/resources/extensions/sf/commands-bootstrap.js +++ b/src/resources/extensions/sf/commands-bootstrap.js @@ -19,55 +19,31 @@ const TOP_LEVEL_SUBCOMMANDS = [ { cmd: "quick", desc: "Execute a quick task without full planning overhead" }, { cmd: "discuss", desc: "Discuss architecture and decisions" }, { 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: "dispatch", desc: "Dispatch a specific phase directly" }, { cmd: "history", desc: "View execution history" }, { cmd: "undo", desc: "Revert last completed unit" }, { cmd: "skip", desc: "Prevent a unit from autonomous dispatch" }, - { cmd: "export", desc: "Export milestone or slice results" }, - { cmd: "cleanup", desc: "Remove merged branches or snapshots" }, - { cmd: "mode", desc: "Switch workflow mode (solo/team)" }, + { cmd: "park", desc: "Park a milestone" }, + { cmd: "unpark", desc: "Reactivate a parked milestone" }, { cmd: "prefs", desc: "Manage preferences" }, { cmd: "config", desc: "Set API keys for external tools" }, { cmd: "keys", desc: "API key manager" }, - { cmd: "hooks", desc: "Show configured hooks" }, - { cmd: "run-hook", desc: "Manually trigger a specific hook" }, - { cmd: "skill-health", desc: "Skill lifecycle dashboard" }, + { cmd: "model", desc: "Switch the active session model or open a picker" }, { 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: "forensics", desc: "Examine execution logs" }, { cmd: "init", desc: "Project init wizard" }, { 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: "steer", desc: "Hard-steer plan documents during execution" }, - { cmd: "inspect", desc: "Show SQLite DB diagnostics" }, { 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: "start", desc: "Start a workflow template" }, { cmd: "templates", desc: "List available workflow templates" }, - { cmd: "extensions", desc: "Manage extensions" }, - { - cmd: "codebase", - desc: "Generate, refresh, and inspect the codebase map cache", - }, - { - cmd: "solver-eval", - desc: "Compare raw agent loops against SF autonomous solver control", - }, - { - cmd: "scaffold", - desc: "Inspect or refresh ADR-021 versioned scaffold docs", - }, + { cmd: "workflow", desc: "Inspect or continue workflow state" }, + { cmd: "ship", desc: "Prepare changes for review and delivery" }, + { cmd: "schedule", desc: "Manage time-bound reminders" }, + { cmd: "backlog", desc: "Inspect priority-ordered work" }, ]; function filterStartsWith(partial, options, prefix = "") { const normalizedPrefix = prefix.length > 0 ? `${prefix} ` : ""; diff --git a/src/resources/extensions/sf/commands/catalog.js b/src/resources/extensions/sf/commands/catalog.js index 26f77b197..d56ba2c03 100644 --- a/src/resources/extensions/sf/commands/catalog.js +++ b/src/resources/extensions/sf/commands/catalog.js @@ -11,7 +11,7 @@ import { resolveProjectRoot } from "../worktree.js"; * Comprehensive description of all available SF commands for help text. */ 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([ "settings", @@ -117,7 +117,7 @@ export const TOP_LEVEL_SUBCOMMANDS = [ { cmd: "model", desc: "Switch the active session model or open a picker" }, { 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", @@ -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( - (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( @@ -653,7 +712,7 @@ export function getSfArgumentCompletions(prefix) { parts.push(""); } if (parts.length <= 1) { - return filterOptions(parts[0] ?? "", TOP_LEVEL_SUBCOMMANDS); + return filterOptions(parts[0] ?? "", PUBLIC_TOP_LEVEL_SUBCOMMANDS); } const [command, subcommand = "", third = ""] = parts; if (command === "cmux") { diff --git a/src/resources/extensions/sf/extension-manifest.json b/src/resources/extensions/sf/extension-manifest.json index d04bd5b7d..66894f301 100644 --- a/src/resources/extensions/sf/extension-manifest.json +++ b/src/resources/extensions/sf/extension-manifest.json @@ -56,130 +56,65 @@ "chapter_close" ], "commands": [ - "add-tests", - "ask", "audit", "autonomous", "backlog", "capture", - "chronicle", - "cleanup", - "cmux", - "codebase", "color", "color-char", "color-config", "color-next", "color-set", "config", - "configure-agent", - "control", - "cost", "create-extension", "create-slash-command", - "debug", - "delegate", - "diff", "discuss", - "dispatch", - "do", "doctor", "emoji", "emoji-config", "emoji-history", "emoji-set", - "escalate", - "eval-review", - "experimental", - "extensions", - "extract-learnings", - "fast", - "find", "forensics", "guard-status", "guard-toggle", - "harness", "help", "history", - "hooks", - "implement", "init", - "inspect", - "instructions", - "keep-alive", "keys", "kill", "knowledge", "logs", - "mcp", - "migrate", - "mode", - "model-mode", - "new-milestone", "next", - "notifications", "notify-beep", "notify-focus", "notify-save-global", "notify-say", "notify-status", "notify-threshold", - "parallel", "park", "pause", "permission", "permission-mode", - "permission-profile", - "plan", - "pr-branch", "prefs", "queue", "quick", - "rate", - "recover", "remote", - "rename", "repair", - "research", - "reset-slice", - "rethink", - "rewind", - "run-hook", - "scaffold", - "scan", "schedule", - "search", - "session-report", "setup", "ship", - "show-config", - "sidekicks", - "skill-health", - "skills", "skip", - "solver-eval", "start", "status", - "statusline", - "steer", - "streamer-mode", "subagent", - "tasks", "templates", - "theme", - "todo", - "trajectory", "triage", "undo", - "undo-task", "unpark", - "uok", "update", "usage", "visualize", - "widget", "workflow", - "worktree", "wt" ], "hooks": [ @@ -201,6 +136,11 @@ "turn_start", "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" + ] } } diff --git a/src/resources/extensions/sf/tests/direct-command-surface.test.mjs b/src/resources/extensions/sf/tests/direct-command-surface.test.mjs index 2c8edbd6d..436622cef 100644 --- a/src/resources/extensions/sf/tests/direct-command-surface.test.mjs +++ b/src/resources/extensions/sf/tests/direct-command-surface.test.mjs @@ -5,6 +5,7 @@ import { test } from "vitest"; import guardrails from "../../guardrails/index.js"; import { DIRECT_SF_COMMAND_NAMES, + getSfArgumentCompletions, getSfTopLevelCommandCompletions, } from "../commands/catalog.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(); 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("doctor")); 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("stop")); 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", () => { 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( readFileSync( join( @@ -56,7 +70,10 @@ test("extension_manifest_uses_permission_profile_command_name", () => { ), ); 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); });