feat(slash-commands): list project commands in AGENTS.md block (Phase 1.5)

The chat layer client-side-expands /<name> args, but the agent still
needs to know what commands exist so it can answer "what slash
commands does this project have?" and recognise the
<!-- scarf-slash:<name> --> marker prepended to expanded prompts.

ProjectContextBlock.renderMinimalBlock(...) gains an optional
slashCommandNames parameter; when non-empty, a new "Project slash
commands" bullet lists the names as backticked /<name> entries.

Mac's ProjectAgentContextService.renderBlock(for:) reads the names
via ProjectSlashCommandService.loadCommands(at:).map(\.name) and
emits the same bullet, keeping Mac and iOS block output aligned
where the content overlaps.

iOS chat resetAndStartInProject splits the slash-command load into a
synchronous read on a detached task BEFORE writing the block —
needed because the block has to land on disk before `hermes acp`
boots, and the async load that populates the chat menu would lose
the race.

Verified: ScarfCore, Mac, iOS all build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-25 08:40:15 +02:00
parent 6808adfa98
commit 8a87ff1922
3 changed files with 47 additions and 4 deletions
@@ -107,7 +107,17 @@ public enum ProjectContextBlock {
/// yet. The marker + identity headers match the Mac output byte-
/// for-byte where the content overlaps, so a project scaffolded
/// on iOS round-trips cleanly through the Mac.
public static func renderMinimalBlock(projectName: String, projectPath: String) -> String {
///
/// `slashCommandNames` populates the v2.5 "Project slash commands"
/// line pass nil/empty to omit that line entirely. The names
/// flow into the agent's context so it can answer "what commands
/// do I have available?" and recognise the `<!-- scarf-slash:<name> -->`
/// marker the chat layer prepends to expanded prompts.
public static func renderMinimalBlock(
projectName: String,
projectPath: String,
slashCommandNames: [String]? = nil
) -> String {
var lines: [String] = []
lines.append(beginMarker)
lines.append("## Scarf project context")
@@ -118,6 +128,10 @@ public enum ProjectContextBlock {
lines.append("")
lines.append("- **Project directory:** `\(projectPath)`")
lines.append("- **Dashboard:** `\(projectPath)/.scarf/dashboard.json`")
if let names = slashCommandNames, !names.isEmpty {
let formatted = names.sorted().map { "`/\($0)`" }.joined(separator: ", ")
lines.append("- **Project slash commands:** \(formatted). The user invokes these via the chat slash menu; you'll see the expanded prompt as a normal user message preceded by `<!-- scarf-slash:<name> -->`.")
}
lines.append("")
lines.append("Any content below this block is template- or user-authored; preserve and defer to it for project-specific behavior. Do NOT modify content inside these markers — Scarf rewrites this block on every project-scoped chat start.")
lines.append(endMarker)