//! The Prompts and skills page's generated fragment: the `claude` command
//! line, every prompt a Run sends, and every Factory skill, rendered from the
//! same functions and embedded files the binary uses, so the page can't drift
//! from what it does. Each prompt is also rendered as a Markdown file in
//! `prompts/`, with a README.md listing them. Test-only: the golden-file tests
//! below keep the checked-in fragment in `site/prompts/index.html` and the
//! `prompts/` files up to date.
use std::ffi::OsStr;
use std::fmt::Write;
use include_dir::{Dir, File};
use crate::ci::FailedChecks;
use crate::github::{Check, CheckState};
use crate::harness::{Choice, Harness};
use crate::issue::IssueUrl;
use crate::prompt;
use crate::session::{claude_args, codex_args};
use crate::skills::SKILLS;
use Sender::{ArchitectRun, SecurityRun, Units};
/// A press unit on the home page, which each prompt and skill links back to.
struct Unit {
anchor: &'static str,
name: &'static str,
}
const IMPLEMENT: Unit = Unit {
anchor: "unit-2",
name: "2 · M · Implement",
};
const REVIEW: Unit = Unit {
anchor: "unit-3",
name: "3 · Y · Review",
};
const FINISH: Unit = Unit {
anchor: "unit-4",
name: "4 · K · Finish",
};
/// The placeholder values the prompts are rendered with, highlighted on the
/// page. The issue number can't be text, so a sentinel stands in for it.
const ISSUE_URL: &str = "<Issue URL>";
const SPEC_URL: &str = "<Spec URL>";
const ISSUE_NUMBER: u64 = u64::MAX;
const NUMBER: &str = "<n>";
const BASE: &str = "<base>";
const BRANCH: &str = "<branch>";
const SPEC_BRANCH: &str = "<Spec branch>";
const PR_URL: &str = "<pull request URL>";
const OWN_HEAD: &str = "<own head>";
const CHECK: &str = "<failing check>";
const CHECK_URL: &str = "<check URL>";
const INHERITED_CHECK: &str = "<Inherited failure>";
const BACKGROUND_WORK: &str = "<background work>";
const FOCUS: &str = "<focus>";
const SESSION_ID: &str = "<session id>";
const PROMPT: &str = "<prompt>";
const MODEL: &str = "<model>";
const EFFORT: &str = "<effort>";
const PLACEHOLDERS: [&str; 20] = [
ISSUE_URL,
SPEC_URL,
NUMBER,
BASE,
BRANCH,
SPEC_BRANCH,
PR_URL,
OWN_HEAD,
CHECK,
CHECK_URL,
INHERITED_CHECK,
BACKGROUND_WORK,
FOCUS,
SESSION_ID,
PROMPT,
MODEL,
EFFORT,
prompt::REVIEW_REPORTS_DIRECTORY,
prompt::SECURITY_REVIEW_REPORT_FILE,
prompt::SECURITY_REVIEW_MERGE_BASE,
];
/// Who sends a prompt.
enum Sender {
/// A Run, from one of these press units.
Units(&'static [Unit]),
/// An Architect run, which the press units, a Run's, don't cover.
ArchitectRun,
/// A Security run, before any dispatched Run.
SecurityRun,
}
/// A prompt as the page shows it: when it is sent, by whom, and its text
/// with placeholders.
struct Prompt {
id: &'static str,
title: &'static str,
when: &'static str,
sender: Sender,
text: String,
}
fn prompts() -> Vec<Prompt> {
let issue = IssueUrl {
url: ISSUE_URL.to_string(),
owner: "<owner>".to_string(),
repo: "<repo>".to_string(),
number: ISSUE_NUMBER,
};
let spec = IssueUrl {
url: SPEC_URL.to_string(),
..issue.clone()
};
let check = |name: &str| Check {
name: name.to_string(),
state: CheckState::Failed,
url: Some(CHECK_URL.to_string()),
job: None,
};
let failed = FailedChecks {
own: vec![check(CHECK)],
inherited: Vec::new(),
on_base: Vec::new(),
};
let failed_with_inherited = FailedChecks {
own: vec![check(CHECK)],
inherited: vec![check(INHERITED_CHECK)],
on_base: vec![check(INHERITED_CHECK)],
};
let with_sentinel = vec![
Prompt {
id: "prompt-fresh",
title: "Fresh",
when: "Starts the implement session, which goes on to review and open the pull request, when the Run starts a new Issue branch.",
sender: Units(&[IMPLEMENT]),
text: prompt::fresh(&issue, BASE, BRANCH),
},
Prompt {
id: "prompt-continuation",
title: "Continuation, with no pull request",
when: "Starts the implement session when the Run is a Continuation of an Issue branch that has no pull request.",
sender: Units(&[IMPLEMENT]),
text: prompt::continuation(&issue, BASE, BRANCH, None),
},
Prompt {
id: "prompt-continuation-pr",
title: "Continuation, with an open pull request",
when: "Starts the implement session when the Run is a Continuation of an Issue branch whose pull request is open.",
sender: Units(&[IMPLEMENT]),
text: prompt::continuation(&issue, BASE, BRANCH, Some(PR_URL)),
},
Prompt {
id: "prompt-spec-review",
title: "Spec review",
when: "In a Spec run, starts the Spec review once every Ticket has landed on the Spec branch, before the Spec PR, a draft until then, is marked ready.",
sender: Units(&[REVIEW]),
text: prompt::spec_review(&spec, BASE, SPEC_BRANCH, PR_URL),
},
Prompt {
id: "prompt-architecture-review",
title: "Architecture review",
when: "Starts the Architecture review, the session an Architect run opens with, in a worktree at the head of the Base branch. The line naming the focus is left out when the command gives none.",
sender: ArchitectRun,
text: prompt::architecture_review(BASE, Some(FOCUS)),
},
Prompt {
id: "prompt-security-review",
title: "Security review",
when: "When enabled, runs one guidance review after a Run's opening session or a Spec run's Spec review, before Delivery pushes and enters the Repair loop. Ticket Runs get none; Base fixes get one too. Introduced findings, refusals or incomplete reviews hold Self-merge.",
sender: Units(&[REVIEW]),
text: prompt::security_review(&issue, BASE, BRANCH),
},
Prompt {
id: "prompt-security-audit",
title: "Security audit",
when: "Starts the report-only Security audit in a throwaway worktree at origin's Base branch head. The threat-model line is included when a conventional document exists; artifacts stay under the repository's audit root.",
sender: SecurityRun,
text: prompt::security_audit(
BASE,
"<audited commit>",
std::path::Path::new("<audit root>"),
std::path::Path::new("<output directory>"),
Some("SECURITY.md"),
),
},
Prompt {
id: "prompt-security-reproduction",
title: "Security reproduction",
when: "After a Security audit, tries to reproduce one recorded finding in a fresh throwaway worktree at its audited commit. The test is copied into the private record only after a complete outcome.",
sender: SecurityRun,
text: prompt::security_reproduction(
"<audited commit>",
"<recorded Security finding>",
std::path::Path::new("<test file>"),
),
},
Prompt {
id: "prompt-conflict-repair",
title: "Conflict Repair",
when: "Starts a Repair session when merging the Base branch into the Issue branch leaves conflicts.",
sender: Units(&[FINISH]),
text: prompt::conflict_repair(&issue, BASE, BRANCH, PR_URL),
},
Prompt {
id: "prompt-security-fix",
title: "Security fix publishing",
when: "With fixing allowed, publishes a terse Ticket or Spec with Tickets for the most severe reproduced finding, reusing a private finding's issue. thirdshift checks and marks it ready before dispatching its Run or Spec run.",
sender: SecurityRun,
text: prompt::security_fix(BASE, "<private record URL>", "<recorded Security finding>"),
},
Prompt {
id: "prompt-foreign-conflict-repair",
title: "Conflict Repair, on Foreign commits",
when: "In a Merge run, starts a Repair session when merging Foreign commits from the Issue branch on origin into the local one leaves conflicts.",
sender: Units(&[FINISH]),
text: prompt::conflict_repair(&issue, BRANCH, BRANCH, PR_URL),
},
Prompt {
id: "prompt-review-repair",
title: "Review Repair",
when: "In a Merge run, starts a Repair session once Foreign commits are merged into the Issue branch, to review them from the head the Run last knew as its own before they can be merged.",
sender: Units(&[FINISH]),
text: prompt::review_repair(&issue, BRANCH, PR_URL, OWN_HEAD),
},
Prompt {
id: "prompt-ci-fix-repair",
title: "CI-fix Repair",
when: "Starts a Repair session when CI fails on the pull request's head commit, listing each failed check.",
sender: Units(&[FINISH]),
text: prompt::ci_fix_repair(&issue, BASE, BRANCH, PR_URL, &failed),
},
Prompt {
id: "prompt-ci-fix-repair-inherited",
title: "CI-fix Repair, with Inherited failures",
when: "Starts a Repair session when CI fails on the pull request's head commit and some of the failed checks, but not all, are Inherited failures: they also failed on the Base branch commit the head last merged in, so they are listed apart as not to fix.",
sender: Units(&[FINISH]),
text: prompt::ci_fix_repair(&issue, BASE, BRANCH, PR_URL, &failed_with_inherited),
},
Prompt {
id: "prompt-resume",
title: "Resume",
when: "Continues any session, once, that ended its turn with background work still running, which was killed with it.",
sender: Units(&[IMPLEMENT, REVIEW, FINISH]),
text: prompt::resume(&[BACKGROUND_WORK]),
},
];
with_sentinel
.into_iter()
.map(|prompt| Prompt {
text: prompt
.text
.replace(&format!("#{ISSUE_NUMBER}"), &format!("#{NUMBER}")),
..prompt
})
.collect()
}
/// The command that regenerates the page's fragment and `prompts/`.
const UPDATE: &str = "UPDATE_PROMPTS=1 cargo test prompts_page";
/// The name of `prompts/`'s entry point.
const README: &str = "README.md";
/// A file in `prompts/`: its name, after the prompt's id, and its contents.
struct PromptFile {
name: String,
contents: String,
}
/// Each prompt as a Markdown file for `prompts/`, then the directory's
/// README.md.
fn prompt_files() -> Vec<PromptFile> {
let prompts = prompts();
let mut files: Vec<_> = prompts
.iter()
.map(|prompt| PromptFile {
name: file_name(prompt),
contents: markdown(prompt),
})
.collect();
files.push(PromptFile {
name: README.to_string(),
contents: readme(&prompts),
});
files
}
/// The name of a prompt's file in `prompts/`, after its id.
fn file_name(prompt: &Prompt) -> String {
let name = prompt
.id
.strip_prefix("prompt-")
.unwrap_or_else(|| panic!("the prompt id {} has no prompt- prefix", prompt.id));
format!("{name}.md")
}
/// `prompts/`'s entry point: what the files are, the `claude` command lines
/// they are sent with, a table linking each prompt's file, and where the
/// Factory skills are.
fn readme(prompts: &[Prompt]) -> String {
let resume = prompts
.iter()
.find(|prompt| prompt.id == "prompt-resume")
.expect("there is a Resume prompt");
let mut readme = format!(
"<!-- Generated by src/prompts_page.rs from the prompts' titles and when sentences in src/prompts_page.rs and the claude and codex arguments in src/session.rs; don't edit. Regenerate with {UPDATE} -->
# Session prompts
Every Session prompt thirdshift can start or resume an agent session with, one file each, with placeholders such as `{BRANCH}` where each Run fills in its own values. The [Prompts and skills page](https://thirdshift.app/prompts/) shows the same prompts.
Every session runs `claude` headless in the Run's worktree, in auto permission mode. The Factory skills are linked into the worktree's `.claude/skills/`, where Claude Code finds them, and kept out of git:
```sh
{new}
```
A [Resume]({resume_file}) continues the session that ended:
```sh
{resume}
```
{codex}
| Prompt | When it is sent | File |
| --- | --- | --- |
",
new = command(None),
resume = command(Some(SESSION_ID)),
resume_file = file_name(resume),
codex = codex_note(|code| format!("`{code}`")),
);
for prompt in prompts {
let _ = writeln!(
readme,
"| {title} | {when} | [{name}]({name}) |",
title = prompt.title,
when = prompt.when,
name = file_name(prompt),
);
}
readme.push_str(
"\nThe Factory skills the prompts name are in [`skills/`](../skills/), as thirdshift writes them out.\n",
);
readme
}
/// A prompt as Markdown: a note on where it comes from, then its title, when
/// it is sent, and its text, fenced.
fn markdown(prompt: &Prompt) -> String {
let fence = fence(&prompt.text);
format!(
"<!-- Generated by src/prompts_page.rs from the prompts in src/prompt.rs and their titles and when sentences in src/prompts_page.rs; don't edit. Regenerate with {UPDATE} -->\n\n# {title}\n\n{when}\n\n{fence}\n{text}\n{fence}\n",
title = prompt.title,
when = prompt.when,
text = prompt.text.trim_end_matches('\n'),
)
}
/// A code fence longer than any run of backticks in `text`, so none closes it.
fn fence(text: &str) -> String {
let longest = text.split(|c| c != '`').map(str::len).max().unwrap_or(0);
"`".repeat(longest.max(2) + 1)
}
/// What uses each Factory skill: the unit whose session does, linked, and
/// when, if not always; or the Architecture review, which is no unit of a Run.
fn skill_used_by(skill: &str) -> String {
match skill {
"thirdshift-implement" | "thirdshift-tdd" => unit_links(&[IMPLEMENT]),
"thirdshift-code-review" | "thirdshift-pr" => unit_links(&[REVIEW]),
"thirdshift-resolving-merge-conflicts" => {
format!("{}, in a Repair", unit_links(&[FINISH]))
}
"thirdshift-improve-codebase-architecture" | "thirdshift-codebase-design" => {
"the Architecture review, in an Architect run".to_string()
}
"thirdshift-to-spec" | "thirdshift-to-tickets" => {
"the Architecture review, in an Architect run, or Security fix publishing, in a Security run".to_string()
}
"thirdshift-security-audit" => {
"the Security audit, in a Security run, or a Security review".to_string()
}
_ => panic!("the Factory skill {skill} has no user: add it to skill_used_by"),
}
}
/// The fragment: the command line, the prompts, the skills, and the skills'
/// licence.
fn render() -> String {
let mut html = String::new();
command_line(&mut html);
prompt_section(&mut html);
skills_section(&mut html);
licence_section(&mut html);
html
}
/// The `claude` command line a session starts with, or, given a session id,
/// resumes with, with the page's placeholders.
fn command(resume: Option<&str>) -> String {
let harness = Choice::default();
format!("claude {}", claude_args(&harness, resume, PROMPT).join(" "))
}
/// The `codex` command line a session on a Model and Effort starts with,
/// with the page's placeholders.
fn codex_command() -> String {
let harness = Choice {
harness: Harness::Codex,
model: Some(MODEL.to_string()),
effort: Some(EFFORT.to_string()),
..Choice::default()
};
format!("codex {}", codex_args(&harness, None, PROMPT).join(" "))
}
/// The note on how a session runs on Codex instead, with `code` marking up
/// each piece of code in it.
fn codex_note(code: impl Fn(&str) -> String) -> String {
format!(
"With {harness}, each session runs {command} instead, without {model} or the Effort's {c} where they are not set, and with {resume} before the prompt for a Resume. The Factory skills are linked into {dir}, and a prompt's first line loads its skill as {sigil}.",
harness = code("harness codex"),
command = code(&codex_command()),
model = code(&format!("-m {MODEL}")),
c = code("-c"),
resume = code(&format!("resume {SESSION_ID}")),
dir = code(".agents/skills/"),
sigil = code("$thirdshift-<skill>"),
)
}
fn command_line(html: &mut String) {
let _ = write!(
html,
r##"<section class="sec" aria-labelledby="command-title">
<div class="wrap">
<details class="fold">
<summary class="sec-head"><span class="eyebrow">The command line</span><h2 id="command-title">How each session starts</h2></summary>
<p class="lede">Every session runs <code>claude</code> headless in the Run's worktree, in auto permission mode. The Factory skills are linked into the worktree's <code>.claude/skills/</code>, where Claude Code finds them, and kept out of git. The prompt is one of the templates below.</p>
<pre class="job-text" aria-label="A new session"><code>{new}</code></pre>
<p class="lede">A <a href="#prompt-resume">Resume</a> continues the session that ended:</p>
<pre class="job-text" aria-label="A Resume"><code>{resume}</code></pre>
<p class="lede">{codex}</p>
</details>
</div>
</section>
"##,
new = highlighted(&command(None)),
resume = highlighted(&command(Some(SESSION_ID))),
codex = codex_note(|code| format!("<code>{}</code>", highlighted(code))),
);
}
fn prompt_section(html: &mut String) {
html.push_str(
r##"<section class="sec" aria-labelledby="prompts-title">
<div class="wrap">
<details class="fold">
<summary class="sec-head"><span class="eyebrow">Prompts</span><h2 id="prompts-title">What a Run tells the agent</h2></summary>
<p class="lede">Every prompt a Run can send, with its placeholders highlighted: they change for each Run.</p>
<div class="job-sheets">
"##,
);
for prompt in prompts() {
let _ = write!(
html,
r##" <article class="job-sheet" id="{id}" aria-labelledby="{id}-title">
<details class="fold">
<summary><h3 id="{id}-title">{title}</h3></summary>
<div class="job-body">
<p>{when}</p>
<p class="sent-by">Sent by {sender}</p>
<pre class="job-text"><code>{text}</code></pre>
</div>
</details>
</article>
"##,
id = prompt.id,
title = prompt.title,
when = prompt.when,
sender = sent_by(&prompt.sender),
text = highlighted(&prompt.text),
);
}
html.push_str(" </div>\n </details>\n </div>\n</section>\n");
}
fn skills_section(html: &mut String) {
html.push_str(
r##"<section class="sec" aria-labelledby="skills-title">
<div class="wrap">
<details class="fold">
<summary class="sec-head"><span class="eyebrow">Factory skills</span><h2 id="skills-title">The skills, as the agent reads them</h2></summary>
<p class="lede">Every Factory skill and supporting file, raw, exactly as thirdshift writes it out.</p>
<div class="job-sheets">
"##,
);
let mut skills: Vec<&Dir> = SKILLS.dirs().collect();
skills.sort_by_key(|dir| dir.path());
for skill in skills {
let name = skill.path().to_string_lossy();
let _ = write!(
html,
r##" <article class="job-sheet" id="skill-{name}" aria-labelledby="skill-{name}-title">
<details class="fold">
<summary><h3 id="skill-{name}-title">{name}</h3></summary>
<div class="job-body">
<p class="sent-by">Used by {used_by}</p>
"##,
used_by = skill_used_by(&name),
);
let mut files = Vec::new();
collect_files(skill, &mut files);
// SKILL.md first, as the agent reads it first; then the rest by path.
let skill_md = skill.path().join("SKILL.md");
let is_skill_md = |file: &File| file.path() == skill_md;
files.sort_by_key(|file| (!is_skill_md(file), file.path()));
for file in files {
// Opening a skill shows its SKILL.md; the supporting files stay folded.
file_block(html, file, " ", is_skill_md(file));
}
html.push_str(" </div>\n </details>\n </article>\n");
}
html.push_str(" </div>\n </details>\n </div>\n</section>\n");
}
fn licence_section(html: &mut String) {
html.push_str(
r##"<section class="sec" aria-labelledby="licence-title">
<div class="wrap">
<details class="fold">
<summary class="sec-head"><span class="eyebrow">Licence and credits</span><h2 id="licence-title">Whose skills these are</h2></summary>
<p class="lede">Most Factory skills are adapted from Matt Pocock's skills, under the licence below. The <a href="#skill-thirdshift-pr">thirdshift-pr</a> skill credits Dex Horthy's <code>show-me</code> skill in its <a href="#file-thirdshift-pr-credits-md">CREDITS.md</a>. The <a href="#skill-thirdshift-security-audit">thirdshift-security-audit</a> skill is copied from <a href="https://github.com/cloudflare/security-audit-skill">Cloudflare's security-audit-skill</a>, under its own <a href="#file-thirdshift-security-audit-license">MIT licence</a>, with its upstream repository and commit recorded in <a href="#file-thirdshift-security-audit-credits-md">CREDITS.md</a>.</p>
"##,
);
let mut files: Vec<&File> = SKILLS.files().collect();
files.sort_by_key(|file| file.path());
for file in files {
assert_eq!(
file.path(),
OsStr::new("LICENSE"),
"a file outside any skill needs a place on the page"
);
file_block(html, file, " ", false);
}
html.push_str(" </details>\n </div>\n</section>\n");
}
/// Every file under `dir`, at any depth.
fn collect_files<'a>(dir: &'a Dir<'a>, files: &mut Vec<&'a File<'a>>) {
files.extend(dir.files());
for dir in dir.dirs() {
collect_files(dir, files);
}
}
/// A file's path as a disclosure heading, then its raw text, folded unless `open`.
fn file_block(html: &mut String, file: &File, indent: &str, open: bool) {
let path = file.path().to_string_lossy();
let text = file
.contents_utf8()
.unwrap_or_else(|| panic!("{path} is not UTF-8"));
let id: String = path
.to_lowercase()
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
let _ = write!(
html,
"{indent}<details class=\"fold\"{open}>\n{indent}<summary><h4 id=\"file-{id}\"><code>{path}</code></h4></summary>\n{indent}<pre class=\"job-text\"><code>{text}</code></pre>\n{indent}</details>\n",
open = if open { " open" } else { "" },
path = escape(&path),
text = escape(text),
);
}
/// Who sends a prompt, as its "Sent by" line ends.
fn sent_by(sender: &Sender) -> String {
match sender {
Units(units) => unit_links(units),
ArchitectRun => "an Architect run, before any unit".to_string(),
SecurityRun => "a Security run, before any unit".to_string(),
}
}
/// Links to `units` on the home page, as a list ending "or".
fn unit_links(units: &[Unit]) -> String {
let links: Vec<_> = units
.iter()
.map(|unit| format!(r##"<a href="/#{}">{}</a>"##, unit.anchor, unit.name))
.collect();
match links.split_last() {
Some((last, [])) => last.clone(),
Some((last, rest)) => format!("{} or {last}", rest.join(", ")),
None => String::new(),
}
}
/// `text` escaped, with each placeholder marked up.
fn highlighted(text: &str) -> String {
let mut html = escape(text);
for placeholder in PLACEHOLDERS {
let escaped = escape(placeholder);
html = html.replace(&escaped, &format!(r##"<mark class="ph">{escaped}</mark>"##));
}
html
}
fn escape(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
}
#[cfg(test)]
mod tests {
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use super::*;
const END: &str = "<!-- End of the generated fragment -->\n";
/// The line the page's fragment starts after.
fn begin() -> String {
format!(
"<!-- Generated from the source by src/prompts_page.rs; don't edit. Regenerate with {UPDATE} -->\n"
)
}
/// Whether to regenerate the page's fragment and `prompts/` rather than
/// check them.
fn updating() -> bool {
std::env::var_os("UPDATE_PROMPTS").is_some()
}
/// The golden-file test: the page carries the fragment as rendered now.
/// The crates.io package leaves `site/` out, so it skips there.
#[test]
fn prompts_page_shows_what_the_binary_sends() {
let site = Path::new(env!("CARGO_MANIFEST_DIR")).join("site");
if !site.is_dir() {
eprintln!(
"skipping: no {} (the crates.io package leaves it out)",
site.display()
);
return;
}
let page_path = site.join("prompts/index.html");
let page = fs::read_to_string(&page_path)
.unwrap_or_else(|error| panic!("could not read {}: {error}", page_path.display()));
let begin = begin();
let (before, rest) = page
.split_once(&begin)
.unwrap_or_else(|| panic!("{} has no line {begin}", page_path.display()));
let (checked_in, after) = rest
.split_once(END)
.unwrap_or_else(|| panic!("{} has no line {END}", page_path.display()));
let rendered = render();
if updating() {
fs::write(&page_path, format!("{before}{begin}{rendered}{END}{after}"))
.unwrap_or_else(|error| panic!("could not write {}: {error}", page_path.display()));
return;
}
assert!(
checked_in == rendered,
"{} is out of date with the prompts, the claude and codex arguments or the skills:\n{}\nRegenerate it with: {UPDATE}",
page_path.display(),
diff(checked_in, &rendered, before.lines().count() + 2),
);
}
#[test]
fn publishing_skills_show_security_fix_publishing_as_a_user() {
let html = render();
for name in ["thirdshift-to-spec", "thirdshift-to-tickets"] {
let (_, card) = html
.split_once(&format!("id=\"skill-{name}\""))
.expect("the page lists the publishing skill");
let (card, _) = card.split_once("</article>").unwrap();
let (_, usage) = card
.split_once("Used by ")
.expect("the card names its users");
let (usage, _) = usage.split_once("</p>").unwrap();
assert!(usage.contains("Security fix publishing"), "{name}: {usage}");
assert!(usage.contains("Architecture review"), "{name}: {usage}");
}
}
#[test]
fn prompts_page_shows_cloudflares_security_audit_and_its_mit_license() {
let html = render();
let (_, skill) = html
.split_once("id=\"skill-thirdshift-security-audit\"")
.expect("the page lists thirdshift-security-audit");
let (skill, _) = skill.split_once("</article>").unwrap();
assert!(skill.contains("Security run") && skill.contains("Security review"));
assert!(skill.contains("MIT License"));
assert!(skill.contains("Copyright (c) 2025-2026 Cloudflare, Inc."));
assert!(skill.contains("https://github.com/cloudflare/security-audit-skill"));
assert!(skill.contains("c1c8a8c1471069fb0e188eeaff69b8e8db6564a8"));
assert!(html.contains("href=\"#file-thirdshift-security-audit-license\""));
}
/// The golden-file test for `prompts/`: a file for each prompt and the
/// README.md, as rendered now, and no other. The crates.io package leaves
/// `prompts/` out, so it skips there.
#[test]
fn prompt_files_show_what_the_binary_sends() {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("prompts");
if !dir.is_dir() {
eprintln!(
"skipping: no {} (the crates.io package leaves it out)",
dir.display()
);
return;
}
let files = prompt_files();
if updating() {
write_prompt_files(&dir, &files);
return;
}
let problems = check_prompt_files(&dir, &files);
assert!(
problems.is_empty(),
"{}\nRegenerate them with: {UPDATE}",
problems.join("\n"),
);
}
/// What is wrong with the files in `dir`, compared with `files`: each
/// file that differs or is missing, with a diff, and each stale one.
fn check_prompt_files(dir: &Path, files: &[PromptFile]) -> Vec<String> {
let mut problems = Vec::new();
for file in files {
let path = dir.join(&file.name);
match fs::read_to_string(&path) {
Ok(checked_in) if checked_in == file.contents => {}
Ok(checked_in) => problems.push(format!(
"{} is out of date with the prompts:\n{}",
path.display(),
diff(&checked_in, &file.contents, 1),
)),
Err(error) if error.kind() == ErrorKind::NotFound => problems.push(format!(
"{} is missing:\n{}",
path.display(),
diff("", &file.contents, 1),
)),
Err(error) => panic!("could not read {}: {error}", path.display()),
}
}
for path in stale_files(dir, files) {
problems.push(format!("{} is stale: nothing generates it", path.display()));
}
problems
}
/// Writes `files` into `dir` and deletes the stale ones.
fn write_prompt_files(dir: &Path, files: &[PromptFile]) {
for file in files {
let path = dir.join(&file.name);
fs::write(&path, &file.contents)
.unwrap_or_else(|error| panic!("could not write {}: {error}", path.display()));
}
for path in stale_files(dir, files) {
let removed = if path.is_dir() {
fs::remove_dir_all(&path)
} else {
fs::remove_file(&path)
};
removed.unwrap_or_else(|error| panic!("could not delete {}: {error}", path.display()));
}
}
/// The entries in `dir` that none of `files` is named after.
fn stale_files(dir: &Path, files: &[PromptFile]) -> Vec<PathBuf> {
let entries = fs::read_dir(dir)
.unwrap_or_else(|error| panic!("could not read {}: {error}", dir.display()));
let mut stale: Vec<_> = entries
.map(|entry| {
entry
.unwrap_or_else(|error| panic!("could not read {}: {error}", dir.display()))
.path()
})
.filter(|path| {
!files
.iter()
.any(|file| path.file_name() == Some(OsStr::new(&file.name)))
})
.collect();
stale.sort();
stale
}
/// The lines of `old` and `new` that differ, as `-` and `+` lines by
/// longest common subsequence, numbered as lines of `new` from `first`.
fn diff(old: &str, new: &str, first: usize) -> String {
let (old, new): (Vec<_>, Vec<_>) = (old.lines().collect(), new.lines().collect());
// common[i][j]: the longest common subsequence of old[i..] and new[j..].
let mut common = vec![vec![0usize; new.len() + 1]; old.len() + 1];
for i in (0..old.len()).rev() {
for j in (0..new.len()).rev() {
common[i][j] = if old[i] == new[j] {
common[i + 1][j + 1] + 1
} else {
common[i + 1][j].max(common[i][j + 1])
};
}
}
let (mut i, mut j, mut out) = (0, 0, String::new());
while i < old.len() || j < new.len() {
if i < old.len() && j < new.len() && old[i] == new[j] {
(i, j) = (i + 1, j + 1);
} else if j == new.len() || (i < old.len() && common[i + 1][j] >= common[i][j + 1]) {
let _ = writeln!(out, "{:>5} - {}", first + j, old[i]);
i += 1;
} else {
let _ = writeln!(out, "{:>5} + {}", first + j, new[j]);
j += 1;
}
}
out
}
#[test]
fn diff_shows_only_the_changed_lines() {
assert_eq!(
diff("a\nb\nc\n", "a\nB\nc\nd\n", 1),
" 2 - b\n 2 + B\n 4 + d\n"
);
}
#[test]
fn a_fence_is_longer_than_any_run_of_backticks_in_the_text() {
assert_eq!(fence("no backticks"), "```");
assert_eq!(fence("see `gh run view`"), "```");
assert_eq!(fence("a block:\n```sh\nls\n```\n"), "````");
assert_eq!(fence("``` then `````"), "``````");
}
#[test]
fn a_prompt_file_carries_the_title_the_when_and_the_fenced_text() {
let prompt = Prompt {
id: "prompt-example",
title: "Example",
when: "When an example runs.",
sender: Units(&[IMPLEMENT]),
text: "Say ```hi```.\n".to_string(),
};
let markdown = markdown(&prompt);
let (note, rest) = markdown.split_once("\n\n").unwrap();
assert!(
note.starts_with("<!-- Generated ") && note.contains(UPDATE),
"{note}"
);
assert_eq!(
rest,
"# Example\n\nWhen an example runs.\n\n````\nSay ```hi```.\n````\n"
);
}
#[test]
fn the_prompts_readme_links_every_prompt_file_and_shows_both_command_lines() {
let files = prompt_files();
let readme = &files
.iter()
.find(|file| file.name == README)
.expect("prompts/ has a README.md")
.contents;
assert!(
readme.starts_with("<!-- Generated ") && readme.contains(UPDATE),
"{readme}"
);
assert!(readme.contains("Session prompt"), "{readme}");
assert!(readme.contains(&command(None)), "{readme}");
assert!(readme.contains(&command(Some(SESSION_ID))), "{readme}");
assert!(readme.contains("](../skills/)"), "{readme}");
for prompt in prompts() {
assert!(
readme.contains(&format!("| {} | {} |", prompt.title, prompt.when)),
"{readme}"
);
}
let links: Vec<_> = readme
.lines()
.filter(|line| line.starts_with("| ") && line.contains("]("))
.map(|line| {
let (_, link) = line.rsplit_once("](").unwrap();
link.trim_end_matches(" |").trim_end_matches(')')
})
.collect();
let prompt_names: Vec<_> = files
.iter()
.map(|file| file.name.as_str())
.filter(|name| *name != README)
.collect();
assert_eq!(links, prompt_names);
}
#[test]
fn checking_prompts_names_each_missing_differing_and_stale_file() {
let temp = tempfile::TempDir::new().unwrap();
let dir = temp.path();
fs::write(dir.join("same.md"), "same\n").unwrap();
fs::write(dir.join("changed.md"), "old\n").unwrap();
fs::write(dir.join("stray.md"), "stray\n").unwrap();
fs::create_dir(dir.join("stray-dir")).unwrap();
let file = |name: &str, contents: &str| PromptFile {
name: name.to_string(),
contents: contents.to_string(),
};
let files = [
file("same.md", "same\n"),
file("changed.md", "new\n"),
file("missing.md", "missing\n"),
];
let problems = check_prompt_files(dir, &files);
assert_eq!(problems.len(), 4, "{problems:?}");
assert!(
problems[0].contains("changed.md")
&& problems[0].contains(" 1 - old\n 1 + new\n")
);
assert!(problems[1].contains("missing.md") && problems[1].contains(" 1 + missing\n"));
assert!(problems[2].contains("stray-dir") && problems[2].contains("nothing generates"));
assert!(problems[3].contains("stray.md") && problems[3].contains("nothing generates"));
write_prompt_files(dir, &files);
assert!(check_prompt_files(dir, &files).is_empty());
assert!(!dir.join("stray.md").exists() && !dir.join("stray-dir").exists());
}
#[test]
fn placeholders_are_highlighted_and_fixed_text_is_escaped() {
assert_eq!(
highlighted("Push <branch>; see `gh run view <run-id>` & <n>"),
r##"Push <mark class="ph"><branch></mark>; see `gh run view <run-id>` & <mark class="ph"><n></mark>"##
);
}
}