From 9ace9192648db0a94ff649f6255f181463baf379 Mon Sep 17 00:00:00 2001 From: burtenshaw Date: Tue, 17 Feb 2026 21:51:38 +0100 Subject: [PATCH 1/3] add skills files --- .gitignore | 1 + AGENTS.md | 10 ++++++ templates/setup/AGENTS.md | 10 ++++++ .../popcorn-submission-workflow/SKILL.md | 32 +++++++++++++++++++ templates/setup/submission.py | 20 ++++++++++++ 5 files changed, 73 insertions(+) create mode 100644 AGENTS.md create mode 100644 templates/setup/AGENTS.md create mode 100644 templates/setup/skills/popcorn-submission-workflow/SKILL.md create mode 100644 templates/setup/submission.py diff --git a/.gitignore b/.gitignore index 300478e..baaef0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ submission.* +!templates/setup/submission.py target/ scratch.md *claude diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1ab8e15 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +## Skills +A skill is a local instruction bundle stored in `SKILL.md`. + +### Available skills +- popcorn-submission-workflow: Helps with Popcorn CLI registration, submission setup, submission modes, and file directives. (file: /Users/ben/code/popcorn-cli/.popcorn/skills/popcorn-submission-workflow/SKILL.md) + +### How to use skills +- Load the skill by reading its `SKILL.md` file when user requests match the description. +- Follow progressive disclosure: read only relevant referenced files/scripts as needed. +- Keep the workspace setup aligned with `popcorn setup`. diff --git a/templates/setup/AGENTS.md b/templates/setup/AGENTS.md new file mode 100644 index 0000000..a0f8e5c --- /dev/null +++ b/templates/setup/AGENTS.md @@ -0,0 +1,10 @@ +## Skills +A skill is a local instruction bundle stored in `SKILL.md`. + +### Available skills +- {{SKILL_NAME}}: Helps with Popcorn CLI registration, submission setup, submission modes, and file directives. (file: {{SKILL_PATH}}) + +### How to use skills +- Load the skill by reading its `SKILL.md` file when user requests match the description. +- Follow progressive disclosure: read only relevant referenced files/scripts as needed. +- Keep the workspace setup aligned with `popcorn setup`. diff --git a/templates/setup/skills/popcorn-submission-workflow/SKILL.md b/templates/setup/skills/popcorn-submission-workflow/SKILL.md new file mode 100644 index 0000000..2b794df --- /dev/null +++ b/templates/setup/skills/popcorn-submission-workflow/SKILL.md @@ -0,0 +1,32 @@ +--- +name: {{SKILL_NAME}} +description: Helps prepare and submit popcorn-cli GPU Mode solutions. Use when users ask to set up a project, create a submission template, or run/register submissions. +compatibility: Intended for popcorn-cli repositories with README.md and shell access. +--- + +# Popcorn Submission Workflow + +Use this skill when the user is working on Popcorn CLI submissions and needs a reliable flow from setup to submit. + +## Recommended workflow +1. Ensure the project has a `submission.py` file with POPCORN directives. +2. Register once with `popcorn register discord` (or `github`) if `.popcorn.yaml` is missing. +3. Use `popcorn submit submission.py` for interactive mode, or `popcorn submit --no-tui ...` for scripts/CI. +4. Use `popcorn submissions list/show/delete` to inspect previous runs. + +## Reference: Authentication (from README) + +{{AUTHENTICATION_SECTION}} + +## Reference: Commands (from README) + +{{COMMANDS_SECTION}} + +## Reference: Submission Format (from README) + +{{SUBMISSION_FORMAT_SECTION}} + +## Guardrails +- Keep submissions as a single Python file. +- Prefer POPCORN directives (`#!POPCORN leaderboard ...`, `#!POPCORN gpu ...`) so defaults are embedded. +- Use `test` or `benchmark` mode before `leaderboard` submissions when iterating. diff --git a/templates/setup/submission.py b/templates/setup/submission.py new file mode 100644 index 0000000..2112cf0 --- /dev/null +++ b/templates/setup/submission.py @@ -0,0 +1,20 @@ +#!POPCORN leaderboard grayscale +#!POPCORN gpu A100 + +""" +Popcorn submission template generated by `popcorn setup`. + +README-aligned notes: +- Submissions are a single Python file. +- You can install extra dependencies at runtime with `pip` if needed. +- Submit with: `popcorn submit submission.py` +""" + + +def solution(): + # Replace with your kernel implementation. + return "hello from popcorn" + + +if __name__ == "__main__": + print(solution()) From ad91bc40b8c198cffe72357a2e0459793c412073 Mon Sep 17 00:00:00 2001 From: burtenshaw Date: Tue, 17 Feb 2026 21:52:27 +0100 Subject: [PATCH 2/3] implement a setup command to add skills and symlink --- src/cmd/mod.rs | 8 ++ src/cmd/setup.rs | 278 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 src/cmd/setup.rs diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index 148e74f..b092769 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -6,6 +6,7 @@ use std::path::PathBuf; mod admin; mod auth; +mod setup; mod submissions; mod submit; @@ -103,6 +104,12 @@ enum SubmissionsAction { #[derive(Subcommand, Debug)] enum Commands { + /// Bootstrap this project with Popcorn agent skills and a submission template + Setup { + /// Overwrite files if they already exist + #[arg(long)] + force: bool, + }, Reregister { #[command(subcommand)] provider: AuthProvider, @@ -149,6 +156,7 @@ enum Commands { pub async fn execute(cli: Cli) -> Result<()> { match cli.command { + Some(Commands::Setup { force }) => setup::run_setup(force), Some(Commands::Reregister { provider }) => { let provider_str = match provider { AuthProvider::Discord => "discord", diff --git a/src/cmd/setup.rs b/src/cmd/setup.rs new file mode 100644 index 0000000..c9348d6 --- /dev/null +++ b/src/cmd/setup.rs @@ -0,0 +1,278 @@ +use anyhow::{Context, Result}; +use serde_json::json; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +const SKILL_NAME: &str = "popcorn-submission-workflow"; +const SUBMISSION_FILENAME: &str = "submission.py"; +const SKILL_TEMPLATE: &str = + include_str!("../../templates/setup/skills/popcorn-submission-workflow/SKILL.md"); +const AGENTS_TEMPLATE: &str = include_str!("../../templates/setup/AGENTS.md"); +const SUBMISSION_TEMPLATE: &str = include_str!("../../templates/setup/submission.py"); + +#[derive(Clone, Copy)] +enum ActionStatus { + Created, + Updated, + Skipped, +} + +impl ActionStatus { + fn label(self) -> &'static str { + match self { + Self::Created => "created", + Self::Updated => "updated", + Self::Skipped => "skipped", + } + } +} + +pub fn run_setup(force: bool) -> Result<()> { + let cwd = env::current_dir().context("Failed to determine current directory")?; + let popcorn_dir = cwd.join(".popcorn"); + let skill_dir = popcorn_dir.join("skills").join(SKILL_NAME); + let skill_path = skill_dir.join("SKILL.md"); + let manifest_path = popcorn_dir.join("setup.json"); + let submission_path = cwd.join(SUBMISSION_FILENAME); + let agents_path = cwd.join("AGENTS.md"); + + fs::create_dir_all(&skill_dir).with_context(|| { + format!( + "Failed to create skill directory at {}", + skill_dir.to_string_lossy() + ) + })?; + + let readme_path = cwd.join("README.md"); + let readme_content = fs::read_to_string(&readme_path).unwrap_or_default(); + let skill_markdown = build_skill_markdown(&readme_content); + let skill_status = write_text_file(&skill_path, &skill_markdown, force)?; + + let manifest = json!({ + "schema_version": 1, + "setup_source": "popcorn setup", + "skills": [{ + "name": SKILL_NAME, + "path": format!(".popcorn/skills/{SKILL_NAME}") + }], + "agents": ["codex", "claude"] + }); + let manifest_text = serde_json::to_string_pretty(&manifest)?; + let manifest_status = write_text_file(&manifest_path, &manifest_text, force)?; + + let agents_md = build_agents_markdown(&skill_path); + let agents_status = write_text_file(&agents_path, &agents_md, force)?; + + let codex_link_status = create_agent_skill_view(&cwd, "codex", &skill_dir, force)?; + let claude_link_status = create_agent_skill_view(&cwd, "claude", &skill_dir, force)?; + + let submission_status = write_text_file( + &submission_path, + &build_submission_template(), + force, + )?; + + println!( + "{} {}", + skill_status.label(), + relative_display(&cwd, &skill_path) + ); + println!( + "{} {}", + manifest_status.label(), + relative_display(&cwd, &manifest_path) + ); + println!( + "{} {}", + agents_status.label(), + relative_display(&cwd, &agents_path) + ); + println!( + "{} {}", + codex_link_status.label(), + relative_display( + &cwd, + &cwd.join(".codex").join("skills").join(SKILL_NAME) + ) + ); + println!( + "{} {}", + claude_link_status.label(), + relative_display( + &cwd, + &cwd.join(".claude").join("skills").join(SKILL_NAME) + ) + ); + println!( + "{} {}", + submission_status.label(), + relative_display(&cwd, &submission_path) + ); + + Ok(()) +} + +fn relative_display(cwd: &Path, target: &Path) -> String { + match target.strip_prefix(cwd) { + Ok(relative) => relative.to_string_lossy().to_string(), + Err(_) => target.to_string_lossy().to_string(), + } +} + +fn write_text_file(path: &Path, content: &str, force: bool) -> Result { + let existed_before = path_exists(path); + if existed_before && !force { + return Ok(ActionStatus::Skipped); + } + + if existed_before { + remove_existing_path(path)?; + } + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + fs::write(path, content)?; + if existed_before { + Ok(ActionStatus::Updated) + } else { + Ok(ActionStatus::Created) + } +} + +fn create_agent_skill_view( + cwd: &Path, + agent_name: &str, + skill_source_dir: &Path, + force: bool, +) -> Result { + let agent_skills_dir = cwd.join(format!(".{}", agent_name)).join("skills"); + fs::create_dir_all(&agent_skills_dir)?; + + let link_path = agent_skills_dir.join(SKILL_NAME); + let existed_before = path_exists(&link_path); + if existed_before && !force { + return Ok(ActionStatus::Skipped); + } + + if existed_before { + remove_existing_path(&link_path)?; + } + + let relative_target = PathBuf::from("../../.popcorn/skills").join(SKILL_NAME); + let symlink_result = create_symlink_dir(&relative_target, &link_path); + if symlink_result.is_err() { + copy_dir_all(skill_source_dir, &link_path)?; + } + + if existed_before { + Ok(ActionStatus::Updated) + } else { + Ok(ActionStatus::Created) + } +} + +fn path_exists(path: &Path) -> bool { + fs::symlink_metadata(path).is_ok() +} + +fn remove_existing_path(path: &Path) -> Result<()> { + let metadata = fs::symlink_metadata(path)?; + let file_type = metadata.file_type(); + if file_type.is_symlink() || file_type.is_file() { + fs::remove_file(path)?; + } else if file_type.is_dir() { + fs::remove_dir_all(path)?; + } + Ok(()) +} + +fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let file_type = entry.file_type()?; + let from = entry.path(); + let to = dst.join(entry.file_name()); + if file_type.is_dir() { + copy_dir_all(&from, &to)?; + } else { + fs::copy(from, to)?; + } + } + Ok(()) +} + +#[cfg(unix)] +fn create_symlink_dir(target: &Path, link_path: &Path) -> std::io::Result<()> { + std::os::unix::fs::symlink(target, link_path) +} + +#[cfg(windows)] +fn create_symlink_dir(target: &Path, link_path: &Path) -> std::io::Result<()> { + std::os::windows::fs::symlink_dir(target, link_path) +} + +fn extract_top_level_section(content: &str, heading: &str) -> Option { + let lines: Vec<&str> = content.lines().collect(); + let start = lines + .iter() + .position(|line| line.trim() == heading) + .map(|idx| idx + 1)?; + + let mut end = lines.len(); + for (idx, line) in lines.iter().enumerate().skip(start) { + if line.trim_start().starts_with("## ") { + end = idx; + break; + } + } + + let section = lines[start..end].join("\n").trim().to_string(); + if section.is_empty() { + None + } else { + Some(section) + } +} + +fn build_skill_markdown(readme_content: &str) -> String { + let authentication = extract_top_level_section(readme_content, "## Authentication") + .unwrap_or_else(|| "See project README for authentication details.".to_string()); + let commands = extract_top_level_section(readme_content, "## Commands") + .unwrap_or_else(|| "See project README for command usage.".to_string()); + let submission_format = extract_top_level_section(readme_content, "## Submission Format") + .unwrap_or_else(|| "Submissions are expected as a single Python file.".to_string()); + + render_template( + SKILL_TEMPLATE, + &[ + ("{{SKILL_NAME}}", SKILL_NAME), + ("{{AUTHENTICATION_SECTION}}", &authentication), + ("{{COMMANDS_SECTION}}", &commands), + ("{{SUBMISSION_FORMAT_SECTION}}", &submission_format), + ], + ) +} + +fn build_agents_markdown(skill_path: &Path) -> String { + let skill_path_text = skill_path.to_string_lossy().to_string(); + render_template( + AGENTS_TEMPLATE, + &[("{{SKILL_NAME}}", SKILL_NAME), ("{{SKILL_PATH}}", &skill_path_text)], + ) +} + +fn build_submission_template() -> String { + SUBMISSION_TEMPLATE.to_string() +} + +fn render_template(template: &str, replacements: &[(&str, &str)]) -> String { + let mut output = template.to_string(); + for (needle, value) in replacements { + output = output.replace(needle, value); + } + output +} From 1b4445b3c507bcce0e1a5e0692ce9708b8e858a8 Mon Sep 17 00:00:00 2001 From: burtenshaw Date: Tue, 17 Feb 2026 21:52:40 +0100 Subject: [PATCH 3/3] update readme with new command --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 95a4e06..9f02f9b 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,17 @@ We regularly run competitions with clear due dates but for beginners we will alw ## Commands +### Setup + +Bootstrap a project with Popcorn skill scaffolding and a submission template. You can overwrite existing files with `--force`. + +```bash +# Create project skill scaffolding + submission.py +popcorn setup +``` + +This will create a new agent skill based on the [templates](templates/setup) and add it to your `.claude/skills` or `.codex/skills` directory. + ### Submit Submit a solution to a leaderboard. Supports both TUI (interactive) and plain modes.