import { filter, map } from "std::array"
import { mkdir } from "std::fs"
import { frontmatter } from "std::markdown"
import { exists } from "std::shell"

// `skillsDir` / `commandsDir` raise ONE approval interrupt up front
// ("scan this directory for skills?"). Reading the matched files is part
// of fulfilling that approval, so we glob + read them with the
// non-interrupt primitives (`_glob` / `_read`) rather than the
// interrupt-raising `glob` / `read`. Otherwise every file would raise
// its own std::glob / std::read interrupt, re-prompting the user (and,
// for an agent that bundles 100+ doc files, flooding the handler chain
// and statelog at startup). Only call these primitives where an
// interrupt that covers the read has already been raised.
import { _read } from "agency-lang/stdlib-lib/builtins.js"
import { _glob } from "agency-lang/stdlib-lib/shell.js"
import { _realDir } from "agency-lang/stdlib-lib/fs.js"
import {
  _docsDir,
  _bundledDocsDir,
  _agentSkillsDir,
  _agentPromptsDir,
  _stringifyFrontmatter,
  _refusedSkillName,
  _parseSkillFile,
 } from "agency-lang/stdlib-lib/skills.js"


/** @module
  @summary Load, save, and draft skill files for LLMs, and expand slash-command templates.

  ```ts
  import { skillsDir } from "std::skills"

  node main(request: string) {
    const skills = skillsDir("${cwd()}/skills") with approve
    const reply: string = llm(request, { tools: [skills] })
    print(reply)
  }
  ```

  Put each skill in `skills/<name>/SKILL.md` with `name` and `description`
  frontmatter. The tool lists the skills and lets the model read them on
  demand. Reads through the tool still need approval. Use the `"flat"`
  layout for loose Markdown files, including skills saved by `writeSkill`.
  Use `designSkill` when the user should review and revise a draft first.

  ```ts
  import { commandsDir, expandSlash } from "std::skills"

  static const commands = commandsDir("${cwd()}/.claude/commands") with approve

  node main(message: string) {
    const reply: string = llm(expandSlash(message, commands))
    print(reply)
  }
  ```

  A command file supplies the prompt for `/name`. Put `$ARGUMENTS` in its
  body where the user's arguments should appear.
*/

/** A skill listing entry whose `location` is relative to its scanned directory. */
export type SkillEntry = {
  name: string;
  description: string;
  location: string
}

// Every string spliced into the `<available_skills>` block comes from
// user-authored frontmatter or filenames. Without escaping, a description
// containing `&` or a closing tag would produce malformed markup and could
// inject extra tags into the prompt. Escape every field `renderEntry` adds.
def xmlEscape(s: string): string {
  """
  Escape characters that have special meaning in XML text and attributes.
  """
  let out = s
  out = out.replaceAll("&", "&amp;")
  out = out.replaceAll("<", "&lt;")
  out = out.replaceAll(">", "&gt;")
  out = out.replaceAll(`"`, "&quot;")
  out = out.replaceAll("'", "&apos;")
  return out
}

def renderEntry(entry: SkillEntry): string {
  """
  Render a skill listing entry as XML.
  """
  const nameX = xmlEscape(entry.name)
  const descX = xmlEscape(entry.description)
  const locX = xmlEscape(entry.location)
  return """  <skill>
    <name>${nameX}</name>
    <description>${descX}</description>
    <location>${locX}</location>
  </skill>"""
}

def flatEntry(filename: string, parsedFrontmatter: any): SkillEntry {
  """
  Build a flat-layout entry, using its frontmatter name, title, or filename.
  """
  return {
    name: parsedFrontmatter.name ?? parsedFrontmatter.title ?? filename,
    description: parsedFrontmatter.description ?? "",
    location: filename
  }
}

// Providers cap tool-name length: OpenAI at 64 characters, Anthropic at 128.
// Cap to the stricter 64 so a name is accepted everywhere.
/** Maximum length of a generated tool name or a newly saved skill name. */
export static const MAX_TOOL_NAME_LEN = 64

// Bound the file reads performed by each scan.
const MAX_SKILL_FILES = 500

def sanitizeToolSegment(raw: string): string {
  """
  Replace punctuation and whitespace with underscores for use in a tool name.
  """
  let cleaned = raw.replaceAll(re/[^a-zA-Z0-9]+/g, "_")
  while (cleaned.startsWith("_")) {
    cleaned = cleaned.slice(1)
  }
  while (cleaned.endsWith("_")) {
    cleaned = cleaned.slice(0, cleaned.length - 1)
  }
  return cleaned
}

// The final path segments distinguish sibling skills directories.
def capToolName(name: string): string {
  """
  Shorten a tool name to the limit, preserving its final path segments.
  """
  if (name.length <= MAX_TOOL_NAME_LEN) {
    return name
  }
  let capped = name.slice(name.length - MAX_TOOL_NAME_LEN, name.length)
  // Trim an underscore left at the start of a shortened name.
  while (capped.startsWith("_")) {
    capped = capped.slice(1)
  }
  return capped
}

def skillToolName(dir: string): string {
  """
  Derive a tool name from a skills directory.
  """
  let cleaned = sanitizeToolSegment(dir)
  if (cleaned == "") {
    cleaned = "root"
  }
  return capToolName("skills_${cleaned}")
}

// Collapse frontmatter whitespace so one skill cannot occupy multiple listing lines.
def oneLine(text: string): string {
  return text.replace(re/\s+/g, " ").trim()
}

// Use the name as a fallback when it adds information beyond the path.
def briefEntry(entry: SkillEntry): string {
  const description = oneLine(entry.description)
  if (description != "") {
    return "${entry.location} - ${description}"
  }
  const name = oneLine(entry.name)
  if (name == "" || name == entry.location) {
    return entry.location
  }
  return "${entry.location} - ${name}"
}

def standardEntry(skillPath: string, parsedFrontmatter: any): SkillEntry {
  """
  Build a SKILL.md entry, defaulting its name to the skill directory.
  """
  const skillDir = skillPath.replace("/SKILL.md", "")
  let name = skillDir
  if (parsedFrontmatter.name != null && parsedFrontmatter.name != "") {
    name = parsedFrontmatter.name
  }
  return {
    name: name,
    description: parsedFrontmatter.description ?? "",
    location: skillPath
  }
}

@alwaysUnder(dir)
effect std::skills::skillsDir {
  dir: string;
  layout: "flat" | "standard"
}
@alwaysUnder(dir)
effect std::skills::commandsDir {
  dir: string
}

/**
Use cached scan results to build a skills tool without reading the directory
again. Choose `brief` for large catalogs to reduce the listing sent on each
model call. Each later read through the tool requires approval.
*/
export def skillsToolFromEntries(
  dir: string,
  entries: SkillEntry[],
  name: string = "",
  brief: boolean = false,
) {
  """
  Build a skills tool from entries scanned from the same directory.

  @param dir - The scanned directory, or the group's own `dir` for scanSkillsSubdirs results.
  @param name - Override the directory-derived tool name, with punctuation replaced by underscores and a 64-character limit.
  @param brief - List each file as one `path - description` line instead of XML.
  """
  let description = ""
  if (brief) {
    const briefLines = [briefEntry(entry) for entry in entries]
    description = "Read a file by passing its path as `filename` to the tool. Files:\n" + briefLines.join("\n")
  } else {
    const lines = map(entries) as entry {
      return renderEntry(entry)
    }
    const header = "Read a skill file from the skills directory '${dir}' by passing its <location> as `filename` to the tool. Available skills:\n<available_skills>\n"
    const footer = "\n</available_skills>"
    description = header + lines.join("\n") + footer
  }
  // Rename the partially applied `read` to distinguish skills tools.
  let toolName = skillToolName(dir)
  if (name != "") {
    // Apply the same name restrictions to caller-supplied names.
    const explicit = sanitizeToolSegment(name)
    if (explicit != "") {
      toolName = capToolName(explicit)
    }
  }
  return read.partial(dir: dir).describe(description).rename(toolName)
}

// A scan's approval covers listing and reading its files, so use the
// interrupt-free primitives here. Keep `root` fixed at the approved directory
// and pass the subdirectory separately to enforce containment.
@capabilities(fileRead)
def scanEntries(
  root: string,
  sub: string,
  layout: "flat" | "standard",
  recursive: boolean,
  maxFiles: number,
): SkillEntry[] {
  let pattern = "*/SKILL.md"
  if (layout == "flat") {
    pattern = "*.{md,markdown}"
    if (recursive) {
      pattern = "**/*.{md,markdown}"
    }
  }
  const pathsResult = try _glob(root, sub, pattern, maxFiles)
  if (pathsResult is failure) {
    return []
  }
  // Preserve frontmatter switches beside lists and multi-line values in SKILL.md.
  const parsed = map(pathsResult.value) as path {
    const raw = try _read(root, "${sub}/${path}", 0, 0) catch ""
    let data: any = {}
    if (layout == "standard") {
      data = _parseSkillFile(raw).data
    } else {
      data = frontmatter(raw) catch {}
    }
    return {
      path: path,
      data: data
    }
  }
  if (layout == "standard") {
    const usable = filter(parsed, \file -> modelInvocable(file.data))
    return map(usable) as file {
      return standardEntry(file.path, file.data)
    }
  }
  return map(parsed) as file {
    return flatEntry(file.path, file.data)
  }
}

def buildSkillsTool(
  dir: string,
  layout: "flat" | "standard",
  name: string = "",
  recursive: boolean = false,
) {
  return skillsToolFromEntries(dir, scanEntries(dir, ".", layout, recursive, MAX_SKILL_FILES), name)
}

/**
Use this to give a model access to skills without loading every body into
its prompt. The tool description lists each skill's name, description, and
location. The model passes a listed location as `filename` to read it.

Standard skills use `<name>/SKILL.md` with `name` and `description`
frontmatter. A missing or empty name defaults to the directory name.
Skills with `disable-model-invocation: true` stay out of this listing.
Flat skills use loose `.md` or `.markdown` files with a frontmatter
`name` or `title`, falling back to the filename.

Set an explicit tool name for stable naming across machines, or when two
directory-derived names collide after sanitizing and shortening them.
*/
@capabilities(fileRead)
export def skillsDir(
  dir: string,
  layout: "flat" | "standard" = "standard",
  name: string = "",
) raises <std::skills::skillsDir> {
  """
  Build a tool that lets an LLM list and read skills from a directory.

  @param layout - Use "standard" for `<name>/SKILL.md` or "flat" for loose Markdown files.
  @param name - Override the directory-derived tool name, with punctuation replaced by underscores and a 64-character limit.
  """
  const real = try _realDir(dir)?
  return interrupt std::skills::skillsDir("Are you sure you want to scan this directory for skill files?", {
    dir: real,
    layout: layout
  })

  return buildSkillsTool(real, layout, name)
}

/** Pass a group's `dir` and `entries` together to `skillsToolFromEntries`. */
export type SkillGroup = {
  dir: string;
  entries: SkillEntry[]
}

/**
Use this when several agents keep flat-layout skills in separate directories
under one root. Build each agent's tool from its group's `dir` and `entries`.
Every requested subdirectory gets a group, including empty or missing ones.
*/
@capabilities(fileRead)
export def scanSkillsSubdirs(
  root: string,
  subdirs: string[],
): Result<Record<string, SkillGroup>> raises <std::skills::skillsDir> {
  """
  Scan direct subdirectories for flat-layout skills and group them by name.

  @param root - The nonempty directory holding the agents' skills directories.
  @param subdirs - Bare directory names, excluding empty names, `.`, `..`, and path separators.
  """
  if (root.trim() == "") {
    return failure("root must not be empty")
  }
  // Check names before asking for approval so each scan stays under the approved root.
  const bad = filter(subdirs, \sub -> sub == "" || sub == "." || sub == ".." || sub.includes("/") || sub.includes("\\"))
  if (bad.length > 0) {
    return failure("not bare directory names: '${bad.join("', '")}'; scanSkillsSubdirs only scans direct subdirectories of its root")
  }
  // An empty request needs no scan approval.
  if (subdirs.length == 0) {
    return success(Object.create(null))
  }
  // Normalize the directory before approval so saved policy rules match later reads.
  const absRoot = try _realDir(root)?
  return interrupt std::skills::skillsDir("Are you sure you want to scan this directory for skill files?", {
    dir: absRoot,
    layout: "flat"
  })

  // A null prototype keeps names such as "__proto__" as ordinary data keys.
  const out: Record<string, SkillGroup> = Object.create(null)
  for (sub in subdirs) {
    out[sub] = {
      dir: "${absRoot}/${sub}",
      entries: scanEntries(absRoot, sub, "flat", false, MAX_SKILL_FILES)
    }
  }
  return success(out)
}

/** A prompt template from `commandsDir` or `scanClaudeSkills` for `expandSlash`. */
export type SkillCommand = {
  name: string;
  description: string;
  argHint: string;
  body: string
}

/**
Use `names` when merging folders by precedence: it includes accepted names
before the model and user invocation switches filter `entries` and `commands`.
A manual-only skill can therefore hide a model-invocable skill in another folder.
*/
export type ClaudeSkillsScan = {
  dir: string;
  names: string[];
  entries: SkillEntry[];
  commands: SkillCommand[]
}

// Matches Claude Code, which cuts a skill's description, `when_to_use`
// included, at this many characters in the skill listing.
static const MAX_SKILL_DESCRIPTION_CHARS = 1536

static const TRUE_WORDS = ["true", "yes", "on", "1"]
static const FALSE_WORDS = ["false", "no", "off", "0"]

// Written with `\$` so it is the literal token, not an interpolation.
static const CLAUDE_SKILL_DIR_TOKEN = "\${CLAUDE_SKILL_DIR}"

// A frontmatter switch read the way Claude Code reads its boolean fields: a
// YAML boolean, a number, or a word, in any letter case.
def frontmatterFlagToBool(data: any, key: string, fallback: boolean): boolean {
  if (data == null || data[key] == null) {
    return fallback
  }
  const value = "${data[key]}"
  const word = value.trim().toLowerCase()
  if (TRUE_WORDS.includes(word)) {
    return true
  }
  if (FALSE_WORDS.includes(word)) {
    return false
  }
  return fallback
}

// `disable-model-invocation: true` keeps a skill out of the model's listing.
def modelInvocable(data: any): boolean {
  return !frontmatterFlagToBool(data, "disable-model-invocation", false)
}

// `user-invocable: false` keeps a skill from running as `/name`.
def userInvocable(data: any): boolean {
  return frontmatterFlagToBool(data, "user-invocable", true)
}

def firstNonEmptyLine(text: string): string {
  for (line in text.split("\n")) {
    if (line.trim() != "") {
      return line.trim()
    }
  }
  return ""
}

// Fall back to the body's first nonempty line before appending `when_to_use`.
def skillDescription(data: any, body: string): string {
  let text = "${data.description ?? ""}"
  if (text == "") {
    text = firstNonEmptyLine(body)
  }
  const whenToUse = "${data.when_to_use ?? ""}"
  if (text == "") {
    text = whenToUse
  } else if (whenToUse != "") {
    text = "${text} ${whenToUse}"
  }
  return text.slice(0, MAX_SKILL_DESCRIPTION_CHARS)
}

def commandRecord(name: string, description: string, data: any, body: string): SkillCommand {
  const argHintRaw = data["argument-hint"] ?? ""
  return {
    name: name,
    description: description,
    argHint: "${argHintRaw}",
    body: body
  }
}

/**
Use this when one Claude Code skills folder supplies both an LLM skills
listing and user slash commands. `disable-model-invocation: true` hides a
skill from `entries`. `user-invocable: false` hides it from `commands`.
Spaces in accepted names become dashes. Reserved or invalid names are skipped.

Command bodies include their base directory and replace `${CLAUDE_SKILL_DIR}`
with that path. Other execution settings, such as `allowed-tools`, are ignored.
The folder's scan approval covers both outputs, so reject
`std::skills::skillsDir` to prevent this scan's command loading.
*/
@capabilities(fileRead)
export def scanClaudeSkills(dir: string): Result<ClaudeSkillsScan> raises <std::skills::skillsDir> {
  """
  Scan a Claude Code skills folder for model-readable skills and slash commands.

  @param dir - The folder containing `<name>/SKILL.md` files.
  """
  const real = try _realDir(dir)?
  return interrupt std::skills::skillsDir("Are you sure you want to scan this directory for skill files?", {
    dir: real,
    layout: "standard"
  })

  const pathsResult = try _glob(real, ".", "*/SKILL.md", MAX_SKILL_FILES)?
  let names: string[] = []
  let entries: SkillEntry[] = []
  let commands: SkillCommand[] = []
  for (path in pathsResult) {
    const raw = try _read(real, path, 0, 0) catch ""
    const parsed = _parseSkillFile(raw)
    const data = parsed.data
    const text = parsed.body
    const folder = path.replace("/SKILL.md", "")
    let name = folder
    if (data.name != null && data.name != "") {
      name = data.name
    }
    if (_refusedSkillName(name) || _refusedSkillName(folder)) {
      continue
    }
    // Slash command names cannot contain spaces.
    name = name.replaceAll(" ", "-")
    names.push(name)
    const description = skillDescription(data, text)
    if (modelInvocable(data)) {
      entries.push({ ...standardEntry(path, data), name: name, description: description })
    }
    if (userInvocable(data)) {
      const skillFolder = "${real}/${folder}"
      const body = "Base directory for this skill: ${skillFolder}\n\n" + text.replaceAll(CLAUDE_SKILL_DIR_TOKEN, skillFolder)
      commands.push(commandRecord(name, description, data, body))
    }
  }
  return success({
    dir: real,
    names: names,
    entries: entries,
    commands: commands
  })
}

@alwaysUnder(dir)
effect std::skills::save {
  dir: string;
  name: string;
  content: string
}

// Validate names without rewriting the caller's choice of filename.
def validSkillName(name: string): boolean {
  return name =~ re/^[a-z0-9]+(-[a-z0-9]+)*$/
}

def renderFrontmatter(name: string, description: string): Result<string> {
  if (description.includes("\n") || description.includes("\r")) {
    return failure("the description must be one line")
  }
  const fm = try _stringifyFrontmatter({ name: name, description: description })
  if (fm is failure(fmErr)) {
    return failure("the description cannot be written as frontmatter: ${fmErr}")
  }
  return fm
}

// Validate before asking for approval and normalize the directory for policy matching.
@capabilities(fileRead)
def checkSkillInputs(dir: string, name: string, description: string): Result<string> {
  if (!validSkillName(name)) {
    return failure("skill name '${name}' is invalid: use kebab-case, lowercase letters and digits separated by single hyphens")
  }
  if (name.length > MAX_TOOL_NAME_LEN) {
    return failure("skill name must be at most ${MAX_TOOL_NAME_LEN} characters, got ${name.length}")
  }
  const rendered = renderFrontmatter(name, description)?
  if (dir.trim() == "") {
    return failure("dir must not be empty")
  }
  const absDir = try _realDir(dir)
  if (isFailure(absDir)) {
    return absDir
  }
  if (exists("${name}.md", absDir.value)) {
    return failure("a skill named ${name} already exists in ${absDir.value}")
  }
  return success(absDir.value)
}

/**
Use this when you already have a finished skill. It creates `<name>.md` in
the flat layout, with name and description frontmatter followed by the body.
It refuses to overwrite an existing file.

The save request shows the complete file. Approval data does not revise it.
The directory creation and file write require their own approvals after the
save approval. Use `designSkill` to collect feedback before saving.
*/
@capabilities(fileRead, fileWrite)
export def writeSkill(
  dir: string,
  name: string,
  description: string,
  body: string,
): Result<SkillEntry> raises <std::skills::save, std::mkdir, std::write> {
  """
  Save a new skill after approval without overwriting an existing file.
  Use designSkill if the user should review and revise a draft first.

  @param dir - The nonempty skills directory to write into.
  @param name - An unused name of at most 64 characters, using lowercase letters and digits separated by single hyphens.
  @param description - One line telling the reading agent when to use the skill.
  @param body - The skill's Markdown instructions.
  """
  const checked = checkSkillInputs(dir, name, description)?
  const absDir = checked
  const filename = "${name}.md"
  const fm = renderFrontmatter(name, description)?
  const content = "${fm}\n${body}\n"
  return interrupt std::skills::save(
    "Do you want to save this skill?",
    { dir: absDir, name: name, content: content },
  )

  const made = mkdir(absDir)?
  // Approval can take time; refuse a duplicate created after the initial check.
  const wrote = write(filename, content, absDir, "create-only")?
  return success({ name: name, description: description, location: filename })
}

// The name stays fixed across revisions because it is also the filename.
type SkillDraft = {
  description: string;
  body: string
}

const ACCEPT = "accept"

effect std::skills::review {
  dir: string;
  name: string;
  description: string;
  body: string
}

def draftBrief(name: string, draft: SkillDraft, feedback: string): string {
  return """
Revise a skill file for an AI agent. A skill is a short markdown document the agent reads when the description says it applies: the description is one line saying when to use it, and the body is the guidance itself.

The skill is named `${name}`. Keep the name.

## Current description
${draft.description}

## Current body
${draft.body}

## Feedback from the user
${feedback}

Rewrite the description and the body so the feedback is addressed. Keep everything the feedback does not mention. Reply with the new description (one line) and the new body (markdown).
"""
}

// Keep options local to avoid a dependency cycle with std::agents/lib/shared.
def draftOptions(model: string, provider: string): any {
  const options: any = {}
  if (model != "") {
    options.model = model
    if (provider != "") {
      options.provider = provider
    }
  }
  return options
}

def redraft(
  name: string,
  draft: SkillDraft,
  feedback: string,
  model: string,
  provider: string,
): Result<SkillDraft> {
  const next: SkillDraft! = llm(draftBrief(name, draft, feedback), draftOptions(model, provider))
  if (next is failure(shapeErr)) {
    return failure("the redraft had the wrong shape: ${shapeErr}")
  }
  const rendered = renderFrontmatter(name, next.value.description)
  if (rendered is failure(renderErr)) {
    return failure("the redraft was refused: ${renderErr}")
  }
  return next
}

// Return null for a bare approval or "accept", and nonempty text for feedback.
def askUser(dir: string, name: string, draft: SkillDraft): Result<string | null> {
  const answer = interrupt std::skills::review("Review this skill before it is saved: answer accept, or give feedback for the next draft", {
    dir: dir,
    name: name,
    description: draft.description,
    body: draft.body
  })
  if (answer == null || answer == ACCEPT) {
    return success(null)
  }
  const feedback: string! = answer
  if (feedback is failure) {
    return failure("unrecognized review answer ${JSON.stringify(answer)}: answer \"${ACCEPT}\", or give feedback for the next draft as text")
  }
  if (feedback.value.trim() == "") {
    return failure("a revise answer needs feedback")
  }
  return feedback
}

// Use `feedback` for the next draft unless `done` ends the loop.
type SkillRoundOutcome = {
  done?: Result<SkillEntry>;
  feedback: string
}

def oneSkillRound(
  dir: string,
  name: string,
  draft: SkillDraft,
): SkillRoundOutcome {
  const answer = askUser(dir, name, draft)
  if (answer is failure(rejected)) {
    return { done: failure(rejected), feedback: "" }
  }
  if (answer.value == null) {
    return { done: writeSkill(dir, name, draft.description, draft.body), feedback: "" }
  }
  return { done: null, feedback: answer.value }
}

/**
Give this tool to a model that can write the first draft from the conversation.
The user sees that draft before any revision call. A bare approval or `accept`
accepts it; other nonempty text asks the model to revise the description and body.
The name stays fixed.

Each revised draft gets another review, up to `maxRounds` reviews in total.
Acceptance calls `writeSkill`, which still asks for save and file-operation
approvals. Accepting the first draft makes no revision model call.
*/
export def designSkill(
  dir: string,
  name: string,
  description: string,
  body: string,
  maxRounds: number = 3,
  model: string = "",
  provider: string = "",
): Result<SkillEntry> raises <std::skills::review, std::skills::save, std::mkdir, std::write> {
  """
  Show a draft skill to the user, revise it on their feedback, and save it
  once they accept.

  @param dir - The nonempty skills directory to write into.
  @param name - An unused name of at most 64 characters, using lowercase letters and digits separated by single hyphens.
  @param description - One line telling the reading agent when to use the skill.
  @param body - The first draft of the skill's Markdown instructions.
  @param maxRounds - Maximum number of reviews before returning a failure.
  @param model - Model to use for revisions, or "" for the configured model.
  @param provider - Provider for the model override, ignored when model is "".
  """
  const checked = checkSkillInputs(dir, name, description)?
  const absDir = checked
  let draft: SkillDraft = { description: description, body: body }
  let feedback = ""
  for (round in range(maxRounds)) {
    if (feedback != "") {
      const next = redraft(name, draft, feedback, model, provider)?
      draft = next
    }
    const outcome = oneSkillRound(absDir, name, draft)
    if (outcome.done != null) {
      return outcome.done
    }
    feedback = outcome.feedback
  }
  return failure("gave up after ${maxRounds} reviews. Last feedback:\n${feedback}")
}

type DocsSection = "guide" | "cli" | "diagnostics" | "stdlib" | "agent"

/**
Scan once, then pass the entries to `docsToolFromEntries` to build both full
and brief listings without rereading the packaged documentation.
*/
export def docsEntries(section: DocsSection): SkillEntry[] {
  """
  List the pages in one section of the packaged Agency documentation.
  """
  // Packaged documentation is trusted for startup scanning without approval.
  return scanEntries(_docsDir(section), ".", "flat", true, MAX_SKILL_FILES)
}

/** Reuse one scan to prepare full and brief docs tools for different context sizes. */
export def docsToolFromEntries(
  section: DocsSection,
  entries: SkillEntry[],
  brief: boolean = false,
) {
  """
  Build a documentation tool from pages listed by docsEntries.

  @param section - The section used to obtain the entries.
  @param brief - List each page as one `path - description` line.
  """
  // Reads through the returned tool require the ordinary std::read approval.
  return skillsToolFromEntries(_docsDir(section), entries, "", brief: brief)
}

@capabilities(fileRead)
export def docsSkill(section: DocsSection, brief: boolean = false) {
  """
  Build a tool that lets an LLM read a section of the packaged Agency docs.

  @param brief - List each page as one `path - description` line.
  """
  return docsToolFromEntries(section, docsEntries(section), brief: brief)
}

/** Use this directory to scope a handler or policy that approves packaged docs reads. */
export def bundledDocsDir(): string {
  """
  Return the directory containing the packaged Agency documentation.
  """
  return _bundledDocsDir()
}

@capabilities(fileRead)
export def agentSkill(agent: string) {
  """
  Build a tool that lets an LLM list and read an agent's packaged skills.

  @param agent - The agent's path under the packaged skills directory.
  """
  // Trust packaged skills for startup scanning. Later tool reads require approval.
  // Use a stable tool name that does not depend on the package's installation path.
  const name = "skills_${agent.replace("/", "_")}"
  return buildSkillsTool(_agentSkillsDir(agent), "flat", name)
}

@capabilities(fileRead)
export def agentPrompt(filename: string): Result<string> {
  """
  Read a prompt file packaged with the stdlib agents.

  @param filename - The prompt's path relative to the packaged prompts directory.
  """
  // Trust packaged prompts for reading without approval, confined to their directory.
  return try _read(_agentPromptsDir(), filename, 0, 0)
}

// Slash commands are pure prompt templates. Deferred features (shell
// injection, `$N` positional references, `allowed-tools` pre-approval) are
// tracked in `docs/superpowers/plans/2026-06-05-slash-commands-v2.md`.

// Remove frontmatter and one trailing newline. An unclosed frontmatter
// fence produces an empty body.
def stripFm(raw: string): string {
  if (!raw.startsWith("---\n")) {
    if (raw.endsWith("\n")) {
      return raw.slice(0, raw.length - 1)
    }
    return raw
  }
  const idx = raw.indexOf("\n---\n", 4)
  if (idx == -1) {
    return ""
  }
  let body = raw.slice(idx + 5)
  if (body.endsWith("\n")) {
    body = body.slice(0, body.length - 1)
  }
  return body
}

/**
Load `.claude/commands` prompt templates for use with `expandSlash` before
calling an LLM. Each loose `.md` filename supplies the command name.
The optional `description` and `argument-hint` frontmatter fields default
to empty strings. Files without frontmatter also work.

Relative directories resolve against the working directory. Use a path under
`__dirname` for commands beside the Agency file, or `${cwd()}/.claude/commands`
for commands in the launch directory.

Only the template body, description, and argument hint are used. Fields such
as `allowed-tools` and `model` do not grant permissions or configure execution.
Use `scanClaudeSkills` for the invocation switches in Claude Code SKILL.md files.
*/
@capabilities(fileRead)
export def commandsDir(dir: string): SkillCommand[] raises <std::skills::commandsDir> {
  """
  Load slash-command templates from Markdown files directly under a directory.
  Returns an empty list when the directory is missing or has no command files.
  """
  const real = try _realDir(dir)
  if (real is failure) {
    return []
  }
  return interrupt std::skills::commandsDir("Are you sure you want to scan this directory for slash command files?", {
    dir: real.value
  })

  const pathsResult = try _glob(real.value, ".", "*.md", MAX_SKILL_FILES)
  if (pathsResult is success(paths)) {
    return map(paths) as p {
      const raw = try _read(real.value, p, 0, 0) catch ""
      // Missing or invalid frontmatter leaves metadata empty.
      const fm = frontmatter(raw) catch {}
      const descriptionRaw = fm.description ?? ""
      let name = p
      if (name.endsWith(".md")) {
        name = name.slice(0, name.length - 3)
      }
      return commandRecord(name, "${descriptionRaw}", fm, stripFm(raw))
    }
  }
  return []
}

/**
```ts
const prompt = expandSlash("/review src/main.ts", commands)
```

Use the expanded prompt as the user's LLM message. The first command with
a matching name supplies its body. `$ARGUMENTS` becomes the trimmed text
after the command name. If the body has no token, extra arguments appear
at the end as `ARGUMENTS: <text>`.

Leading and trailing whitespace do not affect command matching. Unknown
commands and messages without a slash pass through unchanged. Expansion
only substitutes `$ARGUMENTS`; it runs no shell commands and ignores
`$1`-style references.
*/
export def expandSlash(msg: string, commands: SkillCommand[]): string {
  """
  Expand a slash command into its prompt template, substituting $ARGUMENTS.
  Return the original message when no command matches.

  @param commands - Templates from commandsDir or scanClaudeSkills, with the first matching name taking precedence.
  """
  const trimmed = msg.trim()
  if (!trimmed.startsWith("/")) {
    return msg
  }
  let nameEnd = trimmed.length
  let i = 1
  while (i < trimmed.length) {
    const c = trimmed[i]
    if (c == " " || c == "\t" || c == "\n") {
      nameEnd = i
      i = trimmed.length
    } else {
      i = i + 1
    }
  }
  const name = trimmed.slice(1, nameEnd)
  let rawArgs = ""
  if (nameEnd < trimmed.length) {
    rawArgs = trimmed.slice(nameEnd + 1).trim()
  }
  for (cmd in commands) {
    if (cmd.name == name) {
      let out = cmd.body.replaceAll("$ARGUMENTS", rawArgs)
      if (!cmd.body.includes("$ARGUMENTS") && rawArgs != "") {
        out = out + "\n\nARGUMENTS: ${rawArgs}"
      }
      return out
    }
  }
  return msg
}
