Skip to main content

Install

npm i -g @antasphere/hackathon exposes the hackathon binary (the package publishes under the Antasphere org’s npm scope; the binary name is unchanged). The published package is a single self-contained bundle (dist/bin.js, built with esbuild): the workspace-internal @app/contract, @app/sdk, @antasphere/cli-core, and commander are inlined at build time, so the published manifest carries zero runtime dependencies — nothing internal or git-pinned leaks into a public install.

Configuration: profiles, flags, environment

Config lives in the shared Antasphere CLI config home (provided by @antasphere/cli-core — one home for the whole tool family), under the hackathon namespace: $XDG_CONFIG_HOME/antasphere/tools/hackathon.json (default ~/.config/antasphere/tools/hackathon.json), directories 0700, file 0600:
Nothing in the file names a workspace: a command names its organization or its workspace itself (below). A profile is an instance. Each profile names one Antasphere Hackathon instance by its baseUrl and holds how you sign in there: the instance’s own key (apiKey), or the keys your Antasphere login was exchanged for there (connectKeys, one per Antasphere login). The profile cloud exists before anything is on disk: it is the Antasphere cloud, https://app.hackathon.antasphere.com, and it carries no baseUrl because that is its default. So a command on a clean machine runs on the cloud. --api-url https://hackathon.example.com selects the profile of that instance, or makes one named for its host (hackathon.example.com; a local instance on port 3000 gets localhost:3000). A self-hosted instance is named once, as a profile. No command renames a profile: the config file is yours, and you can edit a name there. hackathon use <profile> switches the active profile, and hackathon login and hackathon connect make their profile the active one. hackathon profiles lists them: the active mark, the name, the url, and how each signs in:
A profile with no key reads (not signed in). Every command selects its profile in this order: --profile <name>, then the profile of the --api-url / HACKATHON_URL instance, then the active profile (use), then cloud. A key cached by the Antasphere login is only ever sent to the instance its profile names. Every command accepts --api-url (alias --url), --api-key, --api-key-stdin, --profile, --org, --workspace, and --json. Resolution order: Choosing the organization or the workspace: an API key identifies a person, and a person can belong to several workspaces. On the cloud, each workspace is an Antasphere organization. --org <id or name> (or HACKATHON_ORG) names the organization a command runs in, by the id the account site shows or by its name; the instance maps it to its workspace. --workspace <id or name> (or HACKATHON_WORKSPACE) names a workspace of the instance, by the Antasphere Hackathon workspace id hackathon workspaces prints or by its name. A self-hosted instance has no organization ids, so there you use --workspace. Passing both is an error. A name is matched without regard to case; a name that matches several workspaces, or none, is an error that lists the candidates. An id is sent as it is, and a name costs one extra request to look it up. Nothing is saved on the machine: with no selection, the server picks your default workspace. The server’s default is yours to choose: hackathon workspace default <id or name> on a self-hosted instance; on the cloud it is a setting of your Antasphere account, and the command answers with the page. hackathon workspaces lists your memberships, with organization <id> on the rows that have one. hackathon whoami shows the workspace the command ran in, its organization id, what chose it, what the command signed in with, and the Antasphere login (used, not used and why, or not connected). A key pinned to one workspace only ever acts there: selecting another one is refused, and the error names the pin. A file lives in one workspace: a file id asked of another workspace answers “not found”, and the error names the workspace that was asked and what selected it. Keeping secrets out of argv: a value passed as --api-key hck_… is visible to every process on the machine (ps) and lands in the shell history. Two alternatives do not touch the command line: --api-key-stdin reads the key from the first line of stdin (pass show hackathon | hackathon --api-key-stdin files list), HACKATHON_API_KEY carries it in the environment. stdin can only be spent once per invocation — asking twice is a usage error rather than two commands silently sharing one secret.

Cloud instances: one login for the whole family

On an Antasphere-cloud instance you sign in with your Antasphere account, once for every Antasphere tool CLI. hackathon login does it inline (see Sign in); antasphere login, the family entry, does the same when the account CLI is installed. After it, hackathon needs no flag on a clean machine:
When no direct key resolves, the CLI asks discovery (GET /api/v1/instance) whether the instance signs in through the hub (auth.methods contains antasphere); if so, it exchanges the stored Antasphere login for a user-scoped tool-local hck_ key (hub → tool; see your Antasphere account) and caches it in the profile per Antasphere login (connectKeys — ONE key per Antasphere account, valid for every organization; the organization is a per-command selection, --org, never part of the credential).
  • The exchange names no organization (the Antasphere login identifies the USER, never one organization); a single cached key serves whatever organization a command names. Second and later runs make zero hub calls and mint nothing.
  • The hub key is sent to the hub only; the instance sees a short-lived user-scoped JWT (plus its own one-time offline grant, relayed once and never stored by the CLI) and answers with an ordinary local key.
  • A cached key is only ever sent to the instance its profile names (the profile’s baseUrl scopes the cache).
  • A cached key the instance refuses (401: revoked at Antasphere, a membership removed) is evicted and exchanged again once, and the command is retried once. The CLI says so on stderr: The cached key was refused; signed in again through Antasphere.
  • When Antasphere refuses the exchange (for example, an organization not granted the tool: tool_access_denied), the CLI prints Antasphere’s own sentence and exits 3. Exit 1 is a usage or wire error.
  • hackathon logout revokes the cached key(s) server-side (see Sign in), then forgets them.
Self-hosted (oss) instances never take this branch: hackathon login there is the instance’s own email code or a pasted key, and the hub is never contacted.

Sign in

hackathon login is the one sign-in command. With no URL it signs in on the selected profile: the active one, else the cloud. It makes its profile the active one. On the cloud it is your Antasphere account:
When no Antasphere login is stored on this machine, it asks your email and the code sent to it, inline (prompts Antasphere email: and Code: ; --email skips the first). This is the same sign-in antasphere login does, against https://account.antasphere.com (ANTASPHERE_URL overrides it), and the key is stored where antasphere login stores it, so every other Antasphere tool CLI is signed in too. It then exchanges that login for the Antasphere Hackathon key and ends with:
The second line appears only when you have several organizations. A stored Antasphere login that Antasphere no longer accepts is replaced once: the CLI says The stored Antasphere login was rejected: signing in again. and asks for the email and the code. On a self-hosted instance it is the instance’s own email code, both legs in the one command:
The flow needs the instance to have a delivering email driver (EMAIL_DRIVER=smtp|resend|brevo); it signs in existing accounts only — sign-up stays closed (accounts enter via setup or workspace invitations). It mints an hck_ API key server-side (scopes hackathon:read + hackathon:write, never data:export) and saves it on the instance’s profile. Once that profile is active, hackathon login alone signs in there again. Accounts with 2FA enabled are refused (two_factor_required), and an instance with no email driver has no email code at all. For those, mint a key in the dashboard (API keys, Create key; tick hackathon:write, which the dialog leaves unchecked, when the key must also write) and paste it:
A pasted key must start with hck_, and the CLI checks it against the instance before it saves it. --key-name <name> names the key the sign-in mints, as the dashboard lists it, and --expires-in-days <n> gives it a TTL (it never expires otherwise). On the cloud that key is the Antasphere account key; on a self-hosted instance it is the instance key. Profile management:
hackathon logout works on the active profile, or the one --profile names. Every key the profile holds (its own key and each Antasphere login’s key) is revoked on the instance the profile names, never on an --api-url one: DELETE /cli/auth/key, where the presenting key revokes exactly itself. Then the keys are forgotten. The profile stays, with its url. An older instance whose machine allowlist predates the self-revoke refuses it (403): the CLI says the key STAYS VALID server-side; revoke it from the dashboard, and forgets the local copy. The Antasphere login itself stays stored for the other tool CLIs.

Connect and set up (participants)

The participant journey of the hackathon: connect, setup, doctor, beside the chassis’s whoami and logout and the event’s event status. Every one takes --json (the answer on stdout, the progress on stderr, a refusal as { "ok": false, "error": { "code", "message" } }), and every refusal exits with the class of what went wrong (the table below). The commands hold no organizer credential: your own Antasphere login and your own GitHub access do the work. On the cloud nothing names the instance: sign in once, then connect.
connect resolves the instance (--api-url, HACKATHON_URL, the profile, else the cloud, https://app.hackathon.antasphere.com) and the credential through the usual chain (--api-key, HACKATHON_API_KEY, the profile, the cached key, the exchange of the Antasphere login on a cloud instance). When nothing resolves on a cloud instance and no Antasphere login is stored, it says to run hackathon login once, then hackathon connect again (exit 3); on a self-hosted instance it names hackathon login --api-url <url> (exit 3). A stored Antasphere login the hub no longer accepts names hackathon login again (exit 3). When the hub refuses the person (tool_access_denied: no organization of the account opens this tool), the hub’s own sentence is printed as it is, exit 3. Then it asks the server (GET /bootstrap): an account the roster does not name is refused with not_on_roster (exit 3), a workspace with no event with event_not_configured (exit 3). On success it saves the instance on the profile of that instance (cloud on the cloud, the host’s name otherwise), with the key when --api-key or stdin carried it, makes it the active profile, and prints who you are, your team, the repository assignment (not assigned yet until an organizer provisions it), the installer skill’s version and digest, the dashboard URL and the next command (Next: hackathon setup). --json adds defaultDirectory, the folder setup clones into when no --directory is given. An organizer, a jury viewer or an advisor connects fine and is told there is no team app to set up. An assigned repository carries repository.access, your standing on it as GitHub reports it for your GitHub username, read live by the server: standing is no_username (the roster has no GitHub username for you), none (GitHub knows no access: an organizer provisions or resumes the repositories), invited (an invitation waits, accept it at invitationUrl), member (canPush says whether the permission allows a push) or unknown (GitHub did not answer). fix is one sentence of what to do, or null. --github <login> (on connect and on setup) saves your GitHub username on your own roster row (PUT /bootstrap/github), so the team repository invites that account. It is checked first by GitHub’s rule (letters, digits and single hyphens, 39 at most, a leading @ dropped): a login that breaks it is exit 2 and nothing goes on the wire. The command then prints GitHub username: <login> (<standing>) and the rest of the run reads the standing the server answered. An organizer, a jury viewer or an advisor has no repository to be invited on and is refused with not_a_participant (exit 3); a username another person on the roster already named is refused with github_login_taken (exit 3). The participant journey is a plain member’s: a workspace owner or admin is an organizer whatever the roster says, so connect tells them there is no team app to set up even when a roster row seats them on a team. setup runs these steps in order and prints each command it runs on stderr as $ … before running it. Without --directory the clone goes to ~/Antasphere/hackathon/<hackathon-team> (the folder name is the server’s setup.directoryName); --directory <path> puts it anywhere else.
  1. the assignment (connect, then: a participant on a team, with a repository assigned; else exit 3 saying who to ask);
  2. the access gate: repository.access must allow a push. no_username, none, invited, or member without canPush is exit 3 before any command runs, with the server’s fix (or the CLI’s own sentence: run hackathon connect --github <your-username>; accept the invitation at its URL with the account on the roster; ask an organizer). unknown (GitHub did not answer) goes on: the clone tells;
  3. the installer skill, downloaded from the instance and verified against the sha256 digest GetBootstrap announced before a byte of it is read (a mismatch is exit 4 and installs nothing);
  4. Git, Docker, the Docker daemon and Compose (git --version, docker --version, docker info, docker compose version; the first missing or failing one is exit 4 with what to install or start, in the words of your operating system);
  5. the clone: git clone --branch <branch> -- <url> <path> into a missing or empty directory, with an https URL that carries no credential (your Git authentication does the clone; GitHub refusing it is exit 3 with what to accept or configure; a branch the repository does not have is exit 3 naming the organizer);
  6. the resume: a directory that already is the assigned clone (its origin names the same repository over https, ssh or the scp-like form, whatever the case of the owner and the name) is kept as it is (one git fetch origin, never a merge or a reset; local changes are counted and kept); any other non-empty directory is exit 5 and nothing is touched;
  7. the agent files (--agent claude-code, the default, or codex): the instructions file is created only when absent (CLAUDE.md or AGENTS.md, pointing at AGENTS.md); the installer skill is written under the agent’s skills folder (.claude/skills/hackathon-installer/ or .codex/skills/hackathon-installer/), always at the verified version; the template’s own /init skill is reported present or absent;
  8. the template’s runtime needs (hackathon.json: the compose file, the service, the host port, the readiness path; the port --port overrides), the port checked free (a port held by the app of a previous run is not a collision), then docker compose -f <file> -p hackathon-<team> up -d --build and the readiness probe polled every second, --timeout seconds at most (180 by default; exit 4 with the docker compose logs command otherwise). A repository with no hackathon.json yet is not refused: the start is skipped (as with --no-start), the manifest step reads absent, the app is not started by setup, and the answer says so (app: null in JSON). A hackathon.json that is present but invalid is exit 4;
  9. the local URL, the dashboard URL and the diagnostic (--json: the directory, the repository, the installer, the app, the steps).
doctor runs the checks and changes nothing. Without --directory it checks the folder setup uses by default, once the assignment is known. One line per check, in this order, with the fix on a failed one: The fixes are written for the operating system the CLI runs on: Git from the Xcode Command Line Tools or brew install git on macOS, the package manager on Linux, winget install Git.Git on Windows; Node.js with brew install node@22 on macOS, the NodeSource 22.x line on Linux, winget install OpenJS.NodeJS.LTS on Windows (or the installer from nodejs.org everywhere); Docker Desktop on macOS, Docker Engine with sudo systemctl start docker and the docker group on Linux, Docker Desktop with the WSL 2 backend on Windows. The push proof is the one check that asks GitHub from this machine with this machine’s credential. GitHub refusing it (Permission to <owner>/<repo>.git denied to <account>, over HTTPS or SSH) fails with the account GitHub saw and the fix: accept the invitation for your roster username, or sign in with that account (gh auth login, or an SSH key on it). No credential at all (could not read Username, Permission denied (publickey)) fails with no GitHub credential on this machine and the fix gh auth login. Anything else (a network error, a timeout after 60 s) fails in the environment class. The check never runs git push without --dry-run. --json is { "ok", "platform", "checks": [{ "name", "ok", "detail", "fix"?, "cls"? }] }, cls being the exit class a failed check belongs to (access, environment, conflict). Every failure exits with the class of the first failed check. hackathon.json in a team’s repository is what the installer reads and it is validated: version 1, app.compose a file name in the repository root, app.service a compose service name, app.port an integer between 1024 and 65535, app.readyPath an absolute path. Nothing in it is a command: the installer runs exactly docker compose -f <file> -p <project> up -d --build with HACKATHON_APP_PORT set to the port, and polls the path.

Exit codes

Every diagnostic the commands print (a Git or Docker output, a detail in the JSON) is redacted first: API keys of the tool family, GitHub tokens, bearer headers and credentials in URLs are replaced by [redacted].

Event

The hackathon of the workspace: one event per workspace, its schedule, and its roster. The server clock decides everything: the phase is scheduled before the start, building from the start to the build end, grace from the build end to the freeze, and closed from the freeze on. The freeze is always the build end + 15 minutes; nothing pushed after it counts. The vote opens at the freeze or later. A key needs hackathon:read for event status, event agenda show and event roster show, and hackathon:write for event configure, event agenda set, event roster import, event roster lock and event roster unlock. Everything but event status and event agenda show is for an organizer (a workspace owner or admin, or a roster row with the organizer role). An account that is a member of the workspace but not on the roster is refused with not_on_roster: Your account is not on the roster of this event. Ask an organizer to add your email to the roster.
event status prints the event name, the phase, the server time, the five deadlines (start, build end, freeze, vote open, vote close) as ISO instants, the next deadline with the time left as Hh MMm SSs, your role and team, and one line per team with its submission state. The time left is computed from the server’s time in the answer, never from your machine’s clock. An organizer sees every team, a participant their own. A workspace with no event answers event_not_configured. event configure sends the whole configuration every time. Every option is required except --reason: --expected-version <n> (the version that event status printed; 0 creates the event), --name <name>, --timezone <tz> (an IANA name such as Europe/Paris, for display; every instant is UTC on the wire), --starts-at <iso>, --build-ends-at <iso>, --vote-opens-at <iso> and --vote-closes-at <iso> (ISO 8601 with a zone). It prints Created <name> (version 1) or Configured <name> (version <n>). If someone changed the event since you read it, the refusal names the current version: The event changed since you read it: its version is now 5. Check it with hackathon event status, then pass --expected-version 5. Once the event has started its start cannot move (event_started); moving the build end while it runs needs --reason <text>, which every participant sees (reason_required otherwise); once frozen, the schedule is closed (event_closed), and a build end whose freeze would already be past is refused (build_end_past): a reschedule never closes the event on the spot. Before the start, a change of the start or the build end must start ahead of now (starts_past), so a date typo never closes the event; an event created with a past schedule and never run (nothing was frozen or locked for it) can still be given a real one. Once voting was opened the vote window is fixed (vote_window_locked): community voting close ends the vote early. event roster import --file <path> is declarative: the file IS the roster, and a row absent from it is removed. The file holds { "participants": [...] } or a bare array of entries { "email", "name"?, "role"?, "team"? }: the role is participant (the default, which names a team), organizer, jury or advisor (which name none). A file that cannot be read or is not JSON is refused before any request. The command prints Imported <n> rows, then one Removed <email> (<team>) line per row the import removed, then one Dissolved team <name> line per team no row names any more (its project is archived and leaves the event), so nothing leaves silently. The file is the whole roster: an empty list removes everyone and dissolves every team, with no further confirmation. A locked roster refuses the import (roster_locked); a team whose project an organizer archived through projects archive refuses it too (team_archived): unarchive it or name another team. event roster show prints one row per person: email, role, team (or -), yes or no for whether the person signed in yet, and name; then Roster locked since <iso> when it is locked. event roster lock and event roster unlock are idempotent; unlock is refused once voting has opened (voting_open). Every event command takes --json.
event github --organization <login> --template <owner/repo> [--branch <name>] sets where the teams’ repositories are created: the GitHub organization, the template repository of that organization every team starts from, and the branch the teams push to (--branch, main when left out). It is for an organizer. It prints the organization, the template and the branch, then the next command to run.
event github-check reads the App’s installation on the organization and the template repository, live from GitHub, and lists the problems it finds. It exits non-zero when provisioning cannot run, so a script can stop before admin provision.
event vote --on|--off turns the Vote section on or off. It is off by default. While it is off, every community command and the deck generation (decks generate, decks runs, decks run, decks list, decks show <deckId>, decks review, decks release, decks templates) are refused with vote_off and change nothing, and the dashboard hides the Vote page. decks gallery, decks link and decks show without a deck id still work. Exactly one of --on or --off (neither or both is a usage error, exit 2). It is for an organizer.
The programme is the organizers’ agenda of the day (lunch, a talk, the presentations and the jury, the winner announcement), apart from the schedule above: it moves no phase and no deadline, and an item may fall at any time, during the build included. event agenda show prints one line per item, sorted by start: the start and the end as ISO instants (- when the item has no end), the kind and the title. Every attendee reads it, the jury included (hackathon:read). event agenda set --file <path> replaces the whole programme (an organizer, hackathon:write): the file holds { "items": [...] } or a bare array of items { "title", "kind", "startsAt", "endsAt"? }, where the title is one line of 1 to 80 characters, the kind is break, meal, talk, presentations, announcement or other, the instants are ISO 8601 with a zone, and the end, when set, is after the start. At most 30 items; an empty list clears the programme. A file that cannot be read or is not JSON is refused before any request; an item the server refuses is named by its index and field (items.2.endsAt). It prints the number of items, then the programme as show does.

Submissions

What each team pushed, what the platform received, and what it froze at the deadline. A push reaches the platform through the signed GitHub webhook (POST /webhooks/github/{eventId}, configured by the organizer on the organization); the eligible submission is the last accepted push of the designated branch RECEIVED before the freeze, by receipt order, never by commit time: push early and verify the received revision before the deadline. At the freeze a sweep (every minute, at boot, and on demand) captures the selected revision’s bytes as immutable evidence and freezes it; a team that never pushed is frozen on its initial template. A key needs hackathon:read for submissions list, submissions show, submissions failures, submissions artifact and admin provision --dry-run (a dry run is a read), and hackathon:write for submissions reconcile, submissions exception and admin provision --key. A participant reads their own team; everything else is for an organizer (a workspace owner or admin, or a roster row with the organizer role).
submissions list prints the intake sentence (Intake is open…, Submissions are frozen., or Deadline passed, capture requires attention.), the server time, the freeze, the attention count, then one line per team: the state (waiting, capturing, frozen, missing, error), the repository readiness (planned, created, invited, ready, attention, or unassigned), the last received revision with its receipt time (late when it came after the freeze and is not a candidate), the frozen sha (template only when the team submitted only its initial template), the repository and the team. submissions show <project> prints one team: the state, the repository with its designated branch and readiness, the last received revision with its receipt sequence, the frozen evidence, the freeze instant, the attention sentence and the next action when there is one, then the last 50 receipts (sequence, receipt time, verdict with the refusal code, sha, capture state) and the exceptions. Another team’s project answers project_not_found: No such project here, or not one you can read: a team reads its own submission only. submissions failures is the Admin capture failures view: the signature failure counter of the webhook (count, first, last, reason), the projects needing attention with why, the refused deliveries with their code (installation_mismatch, unknown_repository, wrong_branch, deleted, unresolvable_revision), and the late deliveries. submissions reconcile runs the retry-safe sweep now and prints what it did: Reconciled at <iso>: n captured, n frozen, n capturing, n missing, n errors. The cutoff never moves; a retry captures the exact selected sha, never a later HEAD. submissions exception <project> records an organizer exception after the freeze and before voting opens: --reason <text> (required) and exactly one of --sha <sha> (the full 40-hex sha to freeze instead, captured now) or --missing (mark the project missing on purpose); --invalidate-decks and --invalidate-voting state what the correction invalidates. Refused with event_not_closed before the freeze, judging_started once the eligibility is locked or voting opened (unlock the eligibility first, before the open), and capture_failed when GitHub cannot produce the sha (nothing recorded). submissions artifact <project> --dir <path> downloads the frozen evidence, a tar.gz named submission-<sha12>.tar.gz, written contained in the directory (default .); not_frozen while nothing is frozen. admin provision plans with --dry-run (the organization, the template revision, the sample team, one repository per team with its members at maintain and the coaches at read, the coaches listed once with their GitHub usernames, the conflicts) or applies with --key <key>: the same key resumes the same operation (a step already done is skipped, a failed one retried, the invitations and the readback re-run so accepted invitations move a team to ready and a coach added to the roster since is invited on every repository; a new key after a finished run does the same); --sample <projectId> names the team provisioned first and read back before the rest. The roster is the server’s (event roster import). A plan with conflicts is refused (plan_conflicts) with every conflict listed. Every command takes --json.

Decks

Two decks per team, generated from the frozen submission: a jury deck (a five-minute live presentation, six to eight screens) and a community deck (a quick, consistent comparison, three to four screens). A generation run reads every team with frozen evidence, fills both templates from a structured summary of the archive (every claim cites its evidence or is marked unverified), publishes each deck as an exact version and validates it. An organizer reviews every deck, then releases a team’s approved decks to the gallery; nothing reaches the gallery unreviewed. A key needs hackathon:read for decks gallery, decks templates, decks runs, decks run, decks list, decks show and admin generation-status, and hackathon:write for decks link, decks generate, decks review, decks release and admin generate-all-presentations. The generation, the review and the release are refused with vote_off while the Vote section is off (event vote). Every roster role reads the gallery (a participant or a jury viewer sees pending for a deck not released yet); everything else is for an organizer, and a participant is refused with insufficient_event_role: This needs an organizer of the event. Each team also has three links of its own: a demo deck, a presentation deck and a vote presentation, each a Slideless share link the team made (https://<host>/v/<token>/). The Teams page shows them as plain links, Demo deck, Presentation and Vote presentation. The vote presentation is the deck the other participants watch on the Vote page and rank, made with /plugin:vote; a team is a vote candidate once it is set.
decks link --demo <url> --pitch <url> --vote <url> [--clear <link>] [--team <team>] sets the team’s demo deck, presentation and vote presentation links; any flag alone sets that link and keeps the others. --clear demo|pitch|vote (repeatable) clears that link instead; a team whose vote presentation is cleared is no longer a vote candidate until it sets one again. Who acts is read from the signed-in account: a participant sets their own team’s links (no --team; naming another team is refused 403 not_your_team); an organizer names the team by its name or id (without --team: You are not on a team; pass --team <team>.); a coach (advisor) or a jury viewer is refused 403. A link that is not an https Slideless share link, path /v/<token>/, is refused (exit 2 before the request), and so is a link on a host the instance does not allow (400 invalid_deck_link from the server, whose sentence names the allowed Slideless hosts). Once voting has opened, a participant’s --vote is refused 409 vote_link_frozen (exit 1): the vote presentation is fixed, and only an organizer can replace it. It prints the team and its three links, and works whatever the Vote section says. decks show [--team <team>], without a deck id, prints the team’s three links, - for one not set yet, with the same rules as decks link.

Showcase

A team’s own product name (at most 24 characters), tagline (at most 140) and description (at most 4000), plain text, shown on its card on the Teams page and on its team page. The product name is the name of what you built, 24 characters at most; the Vote page shows it on your team’s tile. Who acts is the same as for decks link: a participant their own team, an organizer names it with --team, a coach or a jury viewer refused 403. A key needs hackathon:read for showcase show and hackathon:write for showcase set.
showcase set [--product <name>] [--tagline <text>] [--description <text> | --description-file <path>] [--clear-product] [--clear-tagline] [--clear-description] [--team <team>] sets any of the three; a field left out keeps its value, and an empty value or its --clear-… flag removes it. Nothing to set, a value beside its clear, or a field over its cap is a usage error (exit 2 before the request). showcase show [--team <team>] prints the team, its product name, its tagline and its description, - for one not set. Both take --json.
decks gallery prints one line per team: <team> jury: <state> community: <state>, the state none, generating, review, failed, invalidated, evidence_changed, pending or released vN (followed by (evidence changed) when the released deck is not of the project’s current frozen evidence); a released deck prints its read-only view on the next indented line. While a kind is not released, the gallery ends with the next useful action. decks templates prints the two template contracts (the kind, the schema version, the screen count range, the screen headings) and the Slideless reference each kind is pinned to now, with its version. decks generate starts a generation run over every team with a frozen submission, both kinds, and prints Run <id> started at revision <n>: <n> pending, <n> skipped and one line per skipped job with its reason. Work already completed at the current revision is skipped; --new-revision generates every team again, the old decks kept. Refused with event_not_closed before the freeze, run_in_progress while a run is still processing, and content_pinned for a new revision once the eligibility lock or the open vote pinned a deck. decks runs prints the current revision, then one line per run: the id, the revision, the status (running, done), the creation time and the counts (pending, running, review, failed, skipped). decks run <runId> prints the run head, then one line per job: the team, the kind, the status (pending, running, review, failed, skipped), the attempts, and the last error or the deck’s version. --log also prints each job’s private generation log, indented. run_not_found for an unknown run. decks list prints every deck of the event, every version: the id, the kind, the version, the review state (pending, approved, rejected), the release (released, superseded, not released) and the team. decks show <deckId> prints one deck: the team, the kind, the version, the template it was filled from, the review, the release and the read-only view, the validation findings when there are any, then each screen’s heading and its claims, an unverified claim prefixed [unverified]. deck_not_found for an unknown deck. decks review <deckId> takes exactly one of --approve or --reject (neither or both is a usage error, exit 2) and an optional --note <text> kept on the review record. A released deck the vote pinned cannot be rejected (content_pinned). decks release <projectId> releases the newest approved deck of each kind of the team’s current frozen evidence to the gallery and prints what it released and what it superseded. Refused with not_approved when a kind has neither a released deck nor an approved one, or when its deck of the current evidence still awaits approval, content_pinned once the eligibility lock or the open vote pinned the released deck, and evidence_replaced when the approved deck is not of the project’s current frozen snapshot (an exception replaced the evidence it was generated from). admin generate-all-presentations is decks generate under another name, --new-revision included. admin generation-status --run <id> is decks run <id>, --log included. Every command takes --json.

Chats

Three kinds of channel, one surface: the event’s general channel (the Feed above), one team chat per team, and one request thread per help request. The server decides who reads which from the roster: an organizer and an advisor read every channel; a participant reads the general channel, their own team’s chat and their team’s request threads. Another team’s channel answers channel_not_found, exactly like a channel that does not exist. A jury viewer is refused (insufficient_event_role), an account not on the roster too (not_on_roster). Posts, pagination, the rate limit and the moderation are the Feed’s. A resolved request’s thread is read-only: a post there is refused with request_resolved until someone reopens the request. A key needs hackathon:read for chats list, chats read and chats replies, and hackathon:write for chats post.
chats list prints one row per channel: the kind (general, team, request), the id, then the name (General, the team’s name, or the request’s title), the general channel first, then the team chats, then the request threads, newest first. chats read prints the posts in the shape of feed list, with [advisor] on an advisor’s post; an activity item the platform wrote in a team chat (a help request’s card, a push, a pull request) prints [request], [push] or [pull request] in place of the author, then its one-line summary (--json carries its kind and activity). A thread’s replies are not in the channel’s page, so a post that has some is followed by ↳ <n> replies: hackathon chats replies <channelId> <postId>. chats replies prints one page of that thread, oldest first, in the same shape (post_not_found when the post is not one of the channel’s you can read). chats post takes the body as words or with --file <path>, exactly one of the two, sends one fresh idempotency key per invocation (a retried request lands once) and prints Posted <id> at <iso>. --thread <postId> posts it as a reply in that post’s thread: the post must be the thread’s first post (not_a_thread_root otherwise), and a malformed id is a usage error before any request. Every chats command takes --json.

Requests

A team asks the event’s advisors for help. A participant on a team opens a request for their own team; an advisor claims it (another advisor’s claim is refused with request_claimed; claiming your own claim again is a no-op); the claiming advisor or an organizer unclaims it; a member of the team, the claiming advisor or an organizer resolves it and reopens it (a reopen clears the claim). An organizer and an advisor see every team’s requests; a participant sees their team’s only, and any other request answers request_not_found. Each request has its thread, a chat channel of kind request (chats read <channelId>). A claim or an unclaim on a resolved request is refused with request_resolved; a role that may not act is refused with insufficient_event_role. A key needs hackathon:read for requests list and requests show, and hackathon:write for requests open, requests claim, requests unclaim, requests resolve and requests reopen.
requests list prints one block per request: the id, the status, the opening time and the team, then the title and, when claimed, claimed by <name>; it ends with Next page: --cursor <cursor> when more exist. --status and --limit (1 to 100) are checked before any request. requests open needs --title (one line, 1 to 200 characters) and --body (1 to 4000), both checked before any request, sends one fresh idempotency key per invocation and prints Opened <id> (thread: hackathon chats read <channelId>). The four acts print Claimed|Unclaimed|Resolved|Reopened <id> (now <status>). Every requests command takes --json.

Feed

The event’s general channel: one channel per event, where organizers, participants and advisors post and organizers pin and moderate. A post is plain text, 1 to 4000 characters (newlines kept, no Markdown, no control characters). An organizer, a participant and an advisor read and post; a jury viewer is refused on every feed command (insufficient_event_role: a jury viewer has presentation access only), and an account not on the roster is refused with not_on_roster. Pinning, unpinning, hiding and restoring are for an organizer. Each account may post ten times per minute; the eleventh post is refused (rate_limited: wait, then post again). A key needs hackathon:read for feed show and feed list, and hackathon:write for feed post, feed pin, feed unpin, feed hide and feed restore.
feed list prints one block per post: a line with the ISO time, the author ((erased account) once the account is erased), [organizer] or [advisor] for a post an organizer or an advisor wrote, [pinned] and [hidden: <reason>] when they apply, then the body indented by two spaces. When more posts exist it ends with Next page: --cursor <cursor>. --limit is 1 to 100, checked before any request. feed show prints the channel name, then the pinned posts in the same shape, newest pin first. A hidden post is shown to organizers only, with its reason; a participant never receives it. feed post takes the body as words or with --file <path>, exactly one of the two (both, or neither, is refused before any request), and prints Posted <id> at <iso>. feed hide <postId> --reason <text> needs a reason of 1 to 500 characters, which only organizers see; a pinned post is unpinned and its reactions are cleared when it is hidden (a restore brings it back without them). A hidden post cannot be pinned (post_hidden): run feed restore first. An id that is not a post of this channel answers No such post in this channel. Every feed command takes --json. The pins and the moderation work the same on a post of any chat channel below: pass its post id.

Community

The participants’ vote: an organizer locks the eligibility (the candidate teams and the voters, with every exclusion and its reason), opens the vote, and after the close calculates and releases the result. Every participant with a team in the locked eligibility casts one ballot: a ranking of every other candidate, best first, never their own team. The server clock decides the window: a ballot write is accepted from the vote opening to the vote closing of the event, and never at or after the close. A key needs hackathon:read for community status, community ballot show, community eligibility show, community ballots list and community results show, and hackathon:write for every other command. The ballot commands are a voter’s; community eligibility, community voting, community ballots and community results are an organizer’s. An account that is not on the roster is refused with not_on_roster, as in the Event section.
community status prints the vote state (unlocked, locked, scheduled, open or closed), the eligibility version, the window as two ISO instants, the server time, your role and whether you are a voter (your team, or why not), your ballot line (state, revision, submission time), one line per candidate with your own marked (yours), and the released result when there is one: one row per candidate with its rank, its decimal score, the exact fraction num/den, the number of ballots and the name (- for an unranked candidate), then the notice that one ballot is cast per participant and that this is the community ranking, not the jury result. community ballot show prints the eligibility version, the state, the window, your team, your ballot revision (0 before your first save) and whether you can submit, then the candidates you may rank, numbered, with their project ids, then your draft and your submission as ordered team names. community ballot save --ranking <ids> and community ballot submit --ranking <ids> take the project ids, comma-separated, best first. A draft may be partial or empty; a submission ranks every candidate exactly once, and an empty --ranking is refused before any request. --eligibility-version <n> and --expected-revision <n> name what you read; when either is left out, the command reads your ballot first and uses its eligibility version and revision (0 when you have no ballot). Both must be whole numbers, checked before any request. The commands print Draft saved (revision <n>, accepted at <iso>) or Ballot submitted (revision <n>, accepted at <iso>). A submission sends an Idempotency-Key, a fresh one per invocation, so a retried request counts once. A later submission replaces the previous one; a draft never withdraws a submission. The refusals read as sentences: someone changed your ballot since you read it (revision_conflict: Your ballot changed since you read it: its revision is now 5. Check it with hackathon community ballot show, then pass --expected-revision 5.); the eligibility moved (eligibility_stale, which names the new version); the ranking is not a ballot (invalid_ballot, which names the fault and the project id: a project named twice, your own team, an id that is not one of your candidates, or a candidate left out of a submission); you are not a voter (not_eligible_voter, with the reason); voting is not open yet (voting_not_open) or is closed (voting_closed); no eligibility is locked (eligibility_not_locked); there is no candidate to rank (no_candidates). community eligibility show prints the candidates (included or excluded, the number of members, confirmed or not, ready or not with the readiness answer, the name), the reason of every exclusion, then the voters (included or excluded, email, team, name) with the reason of every exclusion, then Locked (version <n>) since <iso> or Not locked, and Roster locked or Roster not locked. community eligibility lock locks a new version. --file <path> names a JSON file { "excludedProjects"?: [{ "projectId", "reason" }], "excludedVoters"?: [{ "email", "reason" }], "confirmedProjects"?: [projectId] }, checked for shape before any request; without it the lock excludes nothing. --confirm-all reads the current draft first and confirms every included candidate, merged with the file’s list. It prints Eligibility locked (version <n>): <x> candidates (<y> excluded), <z> voters (<w> excluded). A locked eligibility refuses a second lock (eligibility_locked: unlock first), and a project that is no team of the event or an email that is no participant with a team is named in the refusal (invalid_eligibility). community eligibility unlock prints Eligibility unlocked; once voting has opened it is refused (voting_open). community voting open needs the roster locked (roster_not_locked), the eligibility locked (eligibility_not_locked), at least one candidate (no_candidates) and every candidate ready (candidates_not_ready, which lists each project as name: reason); a closed vote does not open again (voting_closed). It prints the state and the window on one line. community voting close prints Voting closed at <iso>; a vote that is not open cannot close (voting_not_open). Both are idempotent. community ballots list prints one row per ballot of the current eligibility: email, state, revision, submission time or -, invalidation reason or -, name. It never shows a ranking, and the read is audited. community ballots invalidate <id> --reason <text> invalidates a submitted ballot after the close, prints Ballot of <email> invalidated, and, when a result already existed, Results recalculated: version <n>. The vote must be closed (voting_not_closed), the ballot submitted (ballot_not_submitted) and not already invalidated (ballot_invalidated); an unknown id is not_found. community results show prints every calculation as version <n> (eligibility v<n>, calculated <iso>, released|unreleased, digest <12 characters>), its rows (rank, decimal score, fraction, ballots, name), one Tie at rank <r>: <names> line per tie, and a coverage line (ballots submitted of the voters, invalidated, candidates and exclusions). community results calculate works after the close only (voting_not_closed) and prints Calculated version <n>, or Version <n> already holds this ballot set when the same ballots were already calculated. community results release <version> takes the version as an argument (a whole number, checked before any request; --version is the program’s own flag) and prints Released version <n>; an unknown version is not_found. Every community command takes --json, which prints the wire payload verbatim.

Workspaces, files, export

Projects

A project is a subgroup of the workspace: a name, a description, its own members with one of three roles (viewer < editor < manager). In the hackathon each team is a project, created by the roster import (event roster import). A workspace’s owners and admins act as managers on every project; any other member creates one and becomes its first manager. A project is archived, never deleted: archived, it leaves the default list and takes no change but its unarchive. A key needs hackathon:read for the reads and hackathon:write for the writes, the same two scopes as every other command.
The projects themselves page like every other listing (--cursor, --limit, --all) and take --json. list shows the live projects; --archived true shows the archived ones and --archived all both. create makes you the project’s first manager and takes --description and --metadata (a JSON object of your own, opaque to the server); update takes --name, --description or --clear-description, and --metadata, which replaces the whole object. archive takes a project out of the default listing and makes it read-only; nothing is ever deleted, and unarchive brings it back. Members come from the workspace’s own roster, named by email (anything with an @) or by user id. add takes a required --role viewer|editor|manager. A manager removes anyone; anyone removes themselves. A stranger’s address is No active member of this workspace matches — check the user id or email. Only a member of the workspace can join a project., and a guest of the workspace is refused with That person is a guest of the workspace, and a guest cannot be a project member. Invite them as a workspace member first. A team is a member too. add --team <team> (a slug or an id, in place of the person) puts one of the workspace’s teams on the project with the role, and every member of the team holds it; role and remove take --team to name a team as their second argument. members list prints a person or team column first, then the user id or the team’s slug. A person’s role on the project is the highest of their own entry and their teams’ entries. The refusals every project verb shares read as sentences: No such project, or it is not yours to read. (A project you are not a member of answers the same way: its existence is not probeable.), You need the manager role on this project to do that., This project is archived and read-only. Unarchive it first to change it., and You are a guest of this workspace, and guests do not take part in projects.

Teams

A team is a named group of the workspace’s people, which a project can take as a member. Every member of the workspace reads the teams; an owner or an admin shapes them. On a workspace managed by the Antasphere account site, the teams are read here and managed there.
list and members read every page, and every verb takes --json (the API answer verbatim). A team from the Antasphere account site is marked (Antasphere) in list. The refusals read as sentences: Only an owner or an admin manages teams., Teams of this workspace are managed on the Antasphere account site: <link>, A team of this workspace already uses the slug "design"., and ada@acme.co is already in this team. On a self-hosted instance whose operator turned demo sign-in on, an owner makes a link that signs one member in without a password, for a demonstration. The commands never use an API key: each one signs in as the owner for its own length (--owner-email or HACKATHON_OWNER_EMAIL; the password from HACKATHON_OWNER_PASSWORD or, with --owner-password-stdin, the first line of stdin) and signs out when it ends. On the cloud edition every demo command says that demo links are minted at the Antasphere hub.
demo link mints one pass per member --email names and prints one link per --path (the first is the pass’s own page, / when none); --hours <n> or --minutes <n> sets the lifetime (a day by default, a week at most). The links are the only place the secret ever appears: keep them like a password until they expire. demo list prints each pass with its expiry, its revocation and its uses; demo revoke <id> ends every session the pass opened.

Paging

Every listing command (projects list, projects members list, feed list, files list) answers one page at a time, newest first: --limit <n> sets the page size (1 to 100) and --cursor <cursor> resumes from the nextCursor a previous page printed. files list and the two project listings also take --all, which follows the cursors until every page is fetched.

Shell completion

Machine use

Add --json to any command except files download for the wire shape; every error prints to stderr and exits non-zero: 1 for a usage or wire error, 3 when Antasphere refuses you (its own sentence is printed). On the cloud, the key variable is not needed once hackathon login has run. Typical agent loop on a self-hosted instance: