Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Its placeholders read as HTML, and it opens without a heading by design.
".github/pull_request_template.md",
// The challenge spec bars a title in intro.md; the Intro screen shows it from the manifest.
"*/intro.md",
"challenges/**/intro.md",
// Fixture Markdown is test data, not documentation.
"test/fixtures/**",
"**/node_modules/**"
Expand Down
29 changes: 17 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# microhackathon-challenges

Portable challenge packages for MLH microhackathons. Each top-level directory
is one challenge package that boothstrap.sh stations and `boothstrap dev` can
run. The repo is plain JavaScript (`.mjs`) on Node 26, and every PR is checked
by the same package validator stations run.
Portable challenge packages for MLH microhackathons. Challenge packages live
under `challenges/`; each one runs on boothstrap.sh stations and with
`boothstrap dev`. The repo is plain JavaScript (`.mjs`) on Node 26, and
every PR is checked by the same package validator stations run.

## Autonomy

Expand Down Expand Up @@ -36,8 +36,12 @@ by the same package validator stations run.

## Challenge packages

- Each top-level directory holding a `challenge.json` is one package, named
after its `id`. No two packages share an id.
- Each directory under `challenges/` holding a `challenge.json` is one
package, named after its `id`; grouping folders such as
`challenges/beginner/<id>` are allowed. Event configs list `challenges/<id>`
(or the nested path). A `challenge.json` at the repo root, directly in
`challenges/`, or in another top-level directory fails verify.
No two packages share an id.
- The contract is boothstrap.sh's `docs/specs/2026-09-26-mvp-challenge-spec.md`,
with `docs/authoring-challenges.md` for the workflow. The spec wins over this
repo's docs. boothstrap.sh is internal: in a cloud session, attach
Expand All @@ -47,11 +51,12 @@ by the same package validator stations run.
- A package's dependencies install through its own `install` hook, never at
the repo root. The root `package.json` has one devDependency,
`@mlhacks/boothstrap`, pinned exactly; bumping it is a one-line PR.
- In-repo symlinks are followed: a symlinked top-level directory holding a
`challenge.json` is a package. A top-level symlink that is absolute,
dangling, looping, or points outside the repo fails verify, as the station
refuses it. `npm run lint` also lints Markdown inside packages, so the house
Markdown rules apply there.
- In-repo symlinks are followed: a symlinked directory under `challenges/`
holding a `challenge.json` is a package. A symlink at the repo root, in
`challenges/`, or in a grouping folder under it that is absolute, dangling,
looping, or points outside the repo fails verify, as the station refuses it.
`npm run lint` also lints Markdown inside packages, so the house Markdown
rules apply there.
- `npm run verify` runs `boothstrap dev --verify` on every package. Add no rule
the station doesn't enforce.
- Test rules, including the network rule, are in
Expand Down Expand Up @@ -82,7 +87,7 @@ by the same package validator stations run.

## Project structure

- `<id>/`: one challenge package per top-level directory, with a
- `challenges/<id>/`: one challenge package per directory, with a
`challenge.json` and its own `README.md`.
- `tools/`: scripts run with `node`; `verify.mjs` runs the validator over every
package.
Expand Down
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ Portable challenge packages for MLH microhackathons.

## Challenges

Each top-level directory is one challenge package, named after its `id`. Every
row links to the package's own README.
Packages live under `challenges/`, one directory per package, named after its
`id`; grouping folders such as `challenges/beginner/<id>` also work. Every row
links to the package's own README.

| Challenge | Description |
| --- | --- |
| [hello-world](hello-world/README.md) | Meet your AI guide and make your first file. |
| [vibe-coding-mad-libs](vibe-coding-mad-libs/README.md) | Fill four blanks, and Pi builds the web app you described. |
| [birdsbirdbirds](birdsbirdbirds/README.md) | Draw a bird and watch it fly across the sky. |
| [hello-world](challenges/hello-world/README.md) | Meet your AI guide and make your first file. |
| [vibe-coding-mad-libs](challenges/vibe-coding-mad-libs/README.md) | Fill four blanks, and Pi builds the web app you described. |
| [birdsbirdbirds](challenges/birdsbirdbirds/README.md) | Draw a bird and watch it fly across the sky. |

Challenges follow the contract in boothstrap.sh's challenge spec and
authoring guide; agents working here start from [AGENTS.md](AGENTS.md).
Expand All @@ -21,8 +22,10 @@ authoring guide; agents working here start from [AGENTS.md](AGENTS.md).
CI runs on every pull request and on every push to `main`, on Ubuntu with
Node 26. It runs `npm ci`, then:

- `npm run verify`: runs `boothstrap dev --verify` on every package and fails
when two packages share an id.
- `npm run verify`: runs `boothstrap dev --verify` on every package under
`challenges/` and fails when two packages share an id or a package sits at
the repo root or in another top-level directory instead of under
`challenges/`.
- `npm test`: the tests (`test/*.test.mjs`); their rules are in
[docs/style/testing.md](docs/style/testing.md).
- `npm run lint`: markdownlint over every Markdown file.
Expand All @@ -36,7 +39,8 @@ npm test
npm run lint
```

A package's dependencies install through its own `install` hook, never at the
To try one package alone, run `npx boothstrap dev challenges/hello-world`. A
package's dependencies install through its own `install` hook, never at the
root.

## Docs
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions hello-world/README.md → challenges/hello-world/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ package has no `install` hook, no Pi extensions, and no MCP servers. Export
`GEMINI_API_KEY`, then set the package path:

```sh
challenge=/absolute/path/to/microhackathon-challenges/hello-world
challenge=/absolute/path/to/microhackathon-challenges/challenges/hello-world
```

Install Pi and the adapter, at the versions boothstrap pins:
Expand All @@ -26,7 +26,7 @@ Load the package by adding its absolute path to `settings.agentPluginPaths` in
```json
{
"settings": {
"agentPluginPaths": ["/absolute/path/to/microhackathon-challenges/hello-world"]
"agentPluginPaths": ["/absolute/path/to/microhackathon-challenges/challenges/hello-world"]
}
}
```
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions docs/style/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ repositories.
test may use is the npm registry, and only to run a package's `install`
hook; everything else stays offline.
- **Fixtures live in `test/fixtures/`.** A fixture is a small repo layout with
one challenge package per top-level directory; a fixture's Markdown is test
data, not documentation.
one challenge package per directory under its `challenges/`; a fixture's
Markdown is test data, not documentation.
- **`test/fixtures/canary.test.mjs` throws if executed.** It proves the test
glob skips fixtures; keep fixture files out of top-level `test/*.test.mjs`.
- **A test's path mirrors the file or flow it covers.**
Expand Down
12 changes: 6 additions & 6 deletions test/birdsbirdbirds.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ import { test } from 'node:test'
import { fileURLToPath } from 'node:url'
import { crc32, deflateSync } from 'node:zlib'

import { DEFAULT_DEBOUNCE_MS, watchWorkspace } from '../birdsbirdbirds/bird-watcher.mjs'
import { checkBird, loadBirdFile, MAX_BIRD_FILE_BYTES, MAX_KEYFRAMES_BYTES } from '../birdsbirdbirds/check-bird.mjs'
import birdsbirdbirdsExtension from '../birdsbirdbirds/extension.mjs'
import { createBirdLabel } from '../birdsbirdbirds/page/bird-label.mjs'
import { createBirdServer, MAX_PNG_BYTES } from '../birdsbirdbirds/server.mjs'
import { DEFAULT_DEBOUNCE_MS, watchWorkspace } from '../challenges/birdsbirdbirds/bird-watcher.mjs'
import { checkBird, loadBirdFile, MAX_BIRD_FILE_BYTES, MAX_KEYFRAMES_BYTES } from '../challenges/birdsbirdbirds/check-bird.mjs'
import birdsbirdbirdsExtension from '../challenges/birdsbirdbirds/extension.mjs'
import { createBirdLabel } from '../challenges/birdsbirdbirds/page/bird-label.mjs'
import { createBirdServer, MAX_PNG_BYTES } from '../challenges/birdsbirdbirds/server.mjs'

const testDirectory = path.dirname(fileURLToPath(import.meta.url))
const packageDirectory = path.join(testDirectory, '..', 'birdsbirdbirds')
const packageDirectory = path.join(testDirectory, '..', 'challenges', 'birdsbirdbirds')
const checkerScript = path.join(packageDirectory, 'check-bird.mjs')
const serverScript = path.join(packageDirectory, 'server.mjs')

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"id": "fixture-gamma",
"title": "Fixture fixture-gamma",
"tagline": "Minimal valid package.",
"type": "terminal",
"agent": "pi",
"model": "google/gemini-2.5-flash",
"vault_services": ["gemini"],
"points": 10,
"estimated_minutes": 2,
"learning_objectives": ["talking to an agent"],
"featured_tech": ["Pi"],
"intro": "intro.md",
"steps": [
{
"id": "hello",
"title": "Say hello",
"hint": "Ask the agent to print hello.",
"done_when": "The agent has printed hello in the terminal."
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# fixture-gamma

Minimal fixture.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "fixture-gamma",
"version": "0.0.0",
"description": "Minimal test fixture."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
name: start-challenge
description: Guides the fixture. Use when the participant starts it.
---

# Guide

Say hello.
22 changes: 22 additions & 0 deletions test/fixtures/roots/outside/challenges/alpha/challenge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"id": "fixture-alpha",
"title": "Fixture fixture-alpha",
"tagline": "Minimal valid package.",
"type": "terminal",
"agent": "pi",
"model": "google/gemini-2.5-flash",
"vault_services": ["gemini"],
"points": 10,
"estimated_minutes": 2,
"learning_objectives": ["talking to an agent"],
"featured_tech": ["Pi"],
"intro": "intro.md",
"steps": [
{
"id": "hello",
"title": "Say hello",
"hint": "Ask the agent to print hello.",
"done_when": "The agent has printed hello in the terminal."
}
]
}
3 changes: 3 additions & 0 deletions test/fixtures/roots/outside/challenges/alpha/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# fixture-alpha

Minimal fixture.
6 changes: 6 additions & 0 deletions test/fixtures/roots/outside/challenges/alpha/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "fixture-alpha",
"version": "0.0.0",
"description": "Minimal test fixture."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
name: start-challenge
description: Guides the fixture. Use when the participant starts it.
---

# Guide

Say hello.
22 changes: 22 additions & 0 deletions test/fixtures/roots/outside/stray/challenge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"id": "fixture-stray",
"title": "Fixture fixture-stray",
"tagline": "Minimal valid package.",
"type": "terminal",
"agent": "pi",
"model": "google/gemini-2.5-flash",
"vault_services": ["gemini"],
"points": 10,
"estimated_minutes": 2,
"learning_objectives": ["talking to an agent"],
"featured_tech": ["Pi"],
"intro": "intro.md",
"steps": [
{
"id": "hello",
"title": "Say hello",
"hint": "Ask the agent to print hello.",
"done_when": "The agent has printed hello in the terminal."
}
]
}
3 changes: 3 additions & 0 deletions test/fixtures/roots/outside/stray/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# fixture-stray

Minimal fixture.
6 changes: 6 additions & 0 deletions test/fixtures/roots/outside/stray/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "fixture-stray",
"version": "0.0.0",
"description": "Minimal test fixture."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
name: start-challenge
description: Guides the fixture. Use when the participant starts it.
---

# Guide

Say hello.
2 changes: 1 addition & 1 deletion test/hello-world.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'

const execFileAsync = promisify(execFile)
const challengeDirectory = fileURLToPath(new URL('../hello-world/', import.meta.url))
const challengeDirectory = fileURLToPath(new URL('../challenges/hello-world/', import.meta.url))

/** Creates a temporary directory that is removed when the test ends. */
const makeDirectory = async (context) => {
Expand Down
Loading
Loading