User Guide — the solver shell

This guide is for someone using the solver: launching the interactive shell, driving it non-interactively, composing commands into blocks, and finding the right command. For exact per-command usage see commands-index.md; for writing solutions see the Solver Guide; for extending the framework see the Developer Guide.


1. Invocation

The shell is built on prompt-toolkit and rich: persistent history, auto-suggest, tab-completion, and typed parameter dispatch.

Three ways to invoke it

Command Where Notes
euler-solver anywhere the launcher the installer drops at ~/.local/bin/euler-solver; it cds into the repo, activates .venv, and execs solver
solver inside the venv the installed console-script entry point (solver.main:main)
python -m solver inside the venv the same entry point via the module's __main__

euler-solver is the convenient form from outside the project — you do not need to activate the virtualenv yourself. solver and python -m solver are equivalent and assume the .venv is already active (source .venv/bin/activate). All three accept the same arguments and modes below; the examples use solver.

Three modes

1. Interactive — a bare invocation with a terminal attached starts the prompt loop:

solver                                 # launch the interactive shell

Inside the shell: ? lists commands, ? <cmd> shows usage, exit or Ctrl-D twice (within a second) quits. Tab completes commands, problem numbers, flags, and Literal values.

2. Piped — when standard input is not a terminal, the shell reads command blocks from stdin, runs them in order, and exits with the last block's status. This is the natural form for scripting against a heredoc or a generated block:

echo "eval 42; benchmark 42" | solver           # pipe a single block

solver <<'EOF'                                  # or a heredoc of several lines
new 42 --py
eval 42 && benchmark 42
EOF

3. Command — pass a command block as an argument; the shell runs it, then exits with its status (no prompt, no stdin read):

solver "eval 42; benchmark 42"         # run a block, then exit with its status
solver "eval 42 && benchmark 42"       # && gates each step on the previous one's success
solver -s "eval 42; benchmark 42"      # -s also tees console output to the session log

Because the exit status propagates, solver "<block>" composes inside an outer shell script or a Makefile target (solver "eval 42 && benchmark 42" && echo ok).

Command-line arguments

Argument Description
-v, --version Print the version and exit
-s, --save Tee console output to the session log (interactive sessions only)
-h, --help Show the argparse help and exit
cmdline A command block to run, then exit with its status; omit for interactive or piped

The web front end

The same shell — plus a solution viewer and an in-browser editor — is served by the deployed web stack, not by a local process you start: a TLS edge, an SRP login, and one service per collaborator. Its architecture and deployment are in web-server-guide.md (make deploy-web).

Signing in and browsing to / gives an xterm.js terminal attached to your own persistent solver shell: it is forked the first time you connect (as your signed-in identity, see §6) and survives disconnect — closing the tab or dropping the connection leaves it running, and reconnecting reattaches to it with its recent output replayed. There is at most one shell per user, so a second browser tab attaches to the same shell (a shared terminal). The shell is torn down when you type exit, log out, or the service stops. /solutions/ and /solutions/NNNN/ are the viewer pages, and they render in the left pane while the terminal keeps its session in the right one.

show N (below) opens a problem's page over the same bridge. From a web shell it swaps the left pane in place; from a local terminal it opens $EULER_BASE_URL (default https://euler.vikasmunshi.com) in a dedicated browser tab named solver-doc, which every later show reuses — refreshing it for the same problem, navigating it for another.

Signed out, the same pages — solutions, docs, blogs — are public: they are served from a packaged copy of the published content, without the private solutions and without a terminal; the right pane is the way to sign in — minimized to the footer until you click Login in the user menu.

The browser's shell is the web shell: the same solver shell, plus the web host's own commands (msg, users, user-authorize, key-split, host-authorize) and a few changes for the page (§3, What the web shell changes). On the web host, the operator's terminal gets those commands from solver-host rather than solver.

The code editor (and the read-only file viewer) is CodeMirror 6, with a line-number gutter, syntax highlighting for Python/C/JSON, and word-wrap always on, so long lines soft-wrap rather than running off-screen. It is progressive enhancement over a plain <textarea>: with JavaScript off, the form still edits and submits through the same save gate.


2. Command blocks

Commands compose with shell-style operators, both at the prompt and inside a quoted cmdline. Every command returns a Unix exit code (0 = success) and the operators gate on it:

  • ; — run the next command unconditionally (sequential; a newline means ;).
  • && — run the next command only if the previous succeeded (exit 0).
  • || — run the next command only if the previous failed (nonzero).
  • { … } — group commands; groups nest.
solver <<'EOF'
new 42 --py && eval 42 && benchmark 42   # stop at the first step that fails
eval 42 || echo "needs work"             # react to a failed evaluation
{ new 42 --py; eval 42 } && benchmark 42 # group, then benchmark only if it succeeded
EOF

A statement can also be a name = expr assignment or a bare expression. An expression sets the exit code from its truthiness (0 when truthy, 1 when falsy), so it can gate a chain.

solver <<'EOF'
limit = 1000                        # assign a user variable
{problem.number} > 100 && echo "a later problem"
EOF

The command language — surface syntax, canonical form, and semantics — is specified end to end in docs/syntax.md.

Variables

Reference any variable with {name} braces; references expand into both command arguments and expressions. User variables are lowercase (limit = 1000). The reserved specials are seeded by the shell:

name type meaning
problem Problem | None the current problem as an object ({problem.number})
rcode int exit code of the most recent evaluation
loop Any the current loop element (None outside a loop)
next int number of the next unsolved problem
random int number of a random unsolved problem
problems list[Problem] every known problem
solved list[Problem] the solved problems
unsolved list[Problem] the unsolved problems
stale list[Problem] problems whose notes are older than their source

next / random are bare problem numbers (handy as arguments: eval {next}); problems / solved / unsolved / stale hold Problem objects — reach a field with an attribute path ({problem.number}, {loop.number}). The computed specials are re-evaluated on every reference, so {random} yields a fresh pick and {solved} reflects current progress each time.

Expressions

Beyond truthiness, expressions support comparisons, arithmetic, list/tuple/set literals, indexing and slicing ({solved}[0:5]), and a fixed set of safe builtins (len, min, max, sum, sorted, range, …). They are side-effect free — there is no assignment inside an expression, only the top-level name = expr statement.

Loops

loop <list>: <block> is a language construct (not a command): it runs the block once per element of a list, binding the special loop to the current element. The list is any expression evaluating to a sequence — the built-in number lists (each sliceable) or a literal.

$ solver <<'EOF'
loop {unsolved}[0:5]: {
  claude-solve {loop.number} solve || break   # work it; stop the loop on failure
}

loop [1, 2, 3]: echo {loop}       # body runs with loop = 1, then 2, then 3
EOF

Each per-iteration step is explicit and gateable. break / continue (or a trailing … || break) control iteration; loops do not nest.


3. The command catalogue

Type ? in the shell for this list, or ? <cmd> for one command's usage. Each command name below links to its full entry — usage and description — in the Command Index. Requires is the least profile that may run the command; every rung above it may too, and below it the command is not registered at all, so it does not appear in your own ? listing. What four columns cannot show — whether a command takes a problem number, whether it supports --silent, what it asks for — is in its index entry, one click along its name.

In every shell — solver, the web shell and solver-host:

Command Aliases Requires Description
! sh, bash contributor Run a shell command from the shell, returning its exit code.
? help reader List every command, or show detailed help for one command.
benchmark — contributor Measure and record the execution time of the problem's solutions.
blog — reader List a blog article's declared tags and what each one maps to.
blogs — reader List a problem's tags and the blog articles that cover them.
check-commands — admin Report every command docstring that breaks the documented standard.
claude — contributor Run Claude Code interactively in the repository root, returning its exit code.
claude-api — contributor Generate one of a problem's solution artifacts through the Claude API.
claude-batch — maintainer Bulk-tag a wave of problems in one Message Batches job, at half price.
claude-blog — maintainer Write (or flesh out) a blog article via the claude-euler-blogger skill.
claude-login — contributor Sign Claude Code in, then report whether it is, returning 0 only when signed in.
claude-solve — contributor Run Claude Code over a problem's solution files, via a skill.
clear cls reader Clear the terminal screen and scrollback, then succeed.
compile-c compile contributor Compile every C solution for the problem into a runnable binary.
costs — contributor Print what this session's AI tokens have cost, per model.
create-blog — maintainer Seed a curated cross-cutting blog page.
echo — reader Print the given text to the console, then succeed.
edit ed contributor Open a solution file in the web code editor.
evaluate eval contributor Evaluate a problem's solutions against its test cases.
gh-merge merge maintainer List the open pull requests, or rebase-merge one onto master.
git-audit audit contributor Audit what git actually stores, across the whole tracked tree.
git-commit commit contributor Stage and commit the named targets — the one commit verb.
git-filter filter reader Report or wire the transparent encryption filter for solutions/private.
git-hooks hooks contributor Run the git pre-commit and (simulated) pre-push checks on demand.
git-identity identity contributor Configure your git identity and push credential from your GitHub login.
git-publish publish maintainer Publish changed files for named targets to the remote repository.
git-push push contributor Push the current branch to origin as yourself, then open its pull request.
git-reset reset reader Soft-reset your branch to origin/master — un-commit, keep every change.
git-status status reader Display the sync state between the local branch and origin/master.
git-sync sync reader Bring the local repository in sync with origin/master.
host-unlock — admin Take the mailed block from host-authorize and unlock this machine.
key-export — admin Print the master key sealed to a machine's public key, to carry there yourself.
key-reconstruct — reader Unwrap the half you were sent, complete it from the repository, store the key.
key-rekey rekey admin Rotate the master key and re-encrypt every private file under the new one.
lint — contributor Lint the problem's solution files, optionally auto-fixing them.
ls — reader List the files in a problem's solution directory.
mark mark-solved contributor Mark the current problem as solved — once its results confirm it.
new — contributor Generate new solution and/or test-case files for the problem.
pause — reader Wait for the user to press Enter before the block carries on.
pip-upgrade upgrade admin Upgrade packages in the current venv for the given dependency groups.
problems — reader Print a list of problems and their count.
progress — reader Print overall progress through the Euler problems.
random — reader Return a random whole number from min to max, both included, as the exit code.
results — reader List the recorded results for a problem.
search find reader Search the solution stack for a case-insensitive regular expression.
show open, view reader Open a problem's page, or any site page, in a browser or the web viewer panel.
sleep — reader Wait the given number of seconds before the block carries on.
summary — maintainer Refresh the solved/unsolved state from your Project Euler progress page.
sys-setup install admin Install or uninstall a system resource.
tags — reader Report over the central tag vocabulary (blogs/tags.json).
test-cases — reader List a problem's test cases.
tool-setup — contributor Install, uninstall or reinstall a per-user tool in your own home.
update-docs — admin Rebuild the registry-generated blocks in the docs/ guides and the README.
update-models — admin Refresh the model catalogue and its pricing.
update-presentation-data — maintainer Refresh the presentation deck's data from the progress file.
update-tags — contributor The glue for the double-entry tag graph.
update-usd-rate — maintainer Refresh the USD→EUR rate used to report API costs.
user — reader Show the solver user, the current identity, and whether it can decrypt.
vault — reader Encrypt this user's id + env at rest under a password-derived vault key.
version — reader Print the installed build version, plus live git detail of the clone.

On the web host only — loaded by the web shell and solver-host, which have the message spool, the roster and the mail relay these need:

Command Aliases Requires Description
host-authorize — admin Mail the master key, sealed to one machine's public key — the off-host grant.
key-split — maintainer Send someone half the master key, sealed to their public key.
logout — reader Sign out of the web session, after a short countdown the notice can skip.
logout-all — admin Sign every web user out, after a countdown that running work can extend.
msg messages reader Read and send messages: what is waiting for you, and what it asks you to do.
user-authorize authorize maintainer Record someone's public key and send them half the master key.
users — admin Administer accounts on the authorization map + the auth service.

What the web shell changes

The web shell runs these same commands with an overlay table on top (packages/webshell/euler_webshell/shell/overlays.toml; docs/package-guide.md § 3.2): a few run a different body — show and edit swap the page's left pane instead of opening a browser tab — and the ones that change state refresh the part of the page they changed. Generated from that table by update-docs.

Command In the web shell
git-commit then refreshes the git chip
git-push then refreshes the git chip
git-sync then refreshes the git chip
git-reset then refreshes the git chip
git-filter then refreshes the git chip
git-publish then refreshes the git chip
key-reconstruct then refreshes the git chip
host-unlock then refreshes the git chip
gh-merge then refreshes the git chip, the message chip
update-docs then refreshes the git chip
update-tags then refreshes the git chip
update-models then refreshes the git chip
update-usd-rate then refreshes the git chip
update-presentation-data then refreshes the git chip
summary then refreshes the solutions grid, the git chip
mark then refreshes the solutions grid, the git chip
user then refreshes the account page
git-identity then refreshes the account page
claude-login then refreshes the account page
tool-setup then refreshes the account page
show runs euler_webshell.shell.viewer.show instead
edit runs euler_webshell.shell.viewer.edit instead
key-rekey runs euler_webshell.shell.keys.key_rekey instead; then refreshes the git chip

The table above is generated from the live command registry by the update-docs command. The same block stands on its own as commands-catalogue.md — the page the web front end renders beside the shell — and commands-index.md carries each command's full usage.


4. Typical sessions

Solve a problem (see the Solver Guide for the full loop):

solver <<'EOF'
show 42     # open problem 42 in the solver-doc browser tab (and select it as the current problem)
pause       # derive/confirm insights (scratch scripts under the solution dir), then continue
new --tc    # create an empty test-case file for the current problem
new --py    # create a template Python solution file (p0042_s0.py)
pause       # implement solve() in the template, then continue
eval        # evaluate the solution against the test cases
pause       # record the answers in the test-case file, then continue
benchmark   # time the solution
mark        # mark the problem as solved
new --c     # create a template C solution file (p0042_s0.c)
pause       # translate the solution to C, then continue
benchmark   # time the C solution too
pause       # document your learnings in notes.html, then continue
EOF

Files are edited in place in the problem's solution directory; commit persists them.

Sweep a range of unsolved problems:

solver <<'EOF'
loop {unsolved}[0:5]: { claude-solve {loop.number} solve }
EOF

5. AI assistance

Two complementary AI paths are wired into the shell, with different requirements: the API path needs the ai dependency group and ANTHROPIC_API_KEY in ~/.euler/env; the Claude Code path needs only the claude CLI (make install-claude, or tool-setup claude), signed in with claude-login:

  • claude-api <target> — a single templated Claude API call that writes one artifact: py / c (a solution), notes (the write-up), or test-cases. It is fast and cheap, and costs tracks its token spend, but the AI neither runs nor verifies its output — that happens afterwards in the script.
  • claude-solve <n> <action> — full Claude Code working directly on a problem's solution files: it runs solver commands, edits files, evaluates, benchmarks, and iterates. The actions are review and solve (e.g. claude-solve 42 solve). Heavier and slower, it is meant for the harder job. ! claude <prompt> drops into an interactive Claude Code session in the repo.

The goal is to deepen understanding, not skip it: generate alternatives after you have solved a problem, or translate your Python into C for comparison.


6. Sessions, history, and config

  • Per-user state. The shell runs as a resolved identity and keeps that user's state under .state/<slug>/ (command history, session log, last active problem). On a local terminal the identity is your OS login name (granted only to the checkout's owner); in the web front end it is your signed-in account, vouched by a one-time shell ticket the server redeems with the auth service at startup (see docs/web-server-guide.md). Each user gets their own history and last problem.
  • History persists across sessions, per user; auto-suggest offers your past lines.
  • Last problem is remembered per user: every shell — interactive, a cmdline block, or piped stdin — restores the problem you last worked on when it starts, and records it as you switch.
  • -s / --save tees the interactive session (typed input + console output) to the session log.
  • Settings are a variable, not a command: {config} prints every one of them ({config.timeout_single} for a single setting), and changing one means editing packages/solver/solver/config/values.conf, which states them all with a comment on each. The Config Guide covers where each setting lives and why.
  • ! <cmd> runs a bash command in the current problem's solution directory (! sh / ! py drop into a shell / Python interpreter).

7. Key Exchange

Solutions under solutions/private/ are encrypted at rest (in accordance with Project Euler's guidelines) by a transparent git clean/smudge filter — they are ciphertext in git but plaintext in your working tree. You only need keys if you want to collaborate on, or study, those files; solutions/public/ is plaintext and needs nothing. The mechanics of the filter itself are covered in the Git Filter Guide; this section covers the key material.

The two keys

~/.euler/id             - your X25519 private key (PKCS8 PEM; machine-local 0600, outside the repo)
~/.euler/enc-key.json   - { "verify": <check ciphertext>, <your-public-key-hex>: <master key wrapped for it> }

~/.euler is a machine-local secrets dir — a sibling dot-directory named for the repo (~/euler → ~/.euler) — kept outside the checkout so secrets never sit in the work tree. Both files are yours alone; nothing about key material is committed or pushed. The private key carries no passphrase (its 0600 permissions, or the per-user vault, are its protection), and the enc-key file is inert without it.

There is a single 32-byte master key, shared by everyone who may read the private solutions and never stored in the clear: each holder keeps their own copy, wrapped to their X25519 public key with an ephemeral ECDH exchange (HKDF-SHA256 + ChaCha20-Poly1305). Loading it reads the private key, unwraps the entry, and proves it against the verify ciphertext before use.

Gaining access (new user)

solver "user"        # creates ~/.euler/id, prints your public key, and files a request

That request goes to the maintainers over the message spool — you do not have to find anyone. When one of them issues the key, it arrives as a message:

msg list                 # the reply is there
msg act <message-id>     # writes ~/.euler/enc-key.json, verified before anything is written

Only a key message from staff is acted on; one from anybody else is shown, never applied.

solver "user" then reports ✓ can encrypt/decrypt, and the private solutions decrypt in place. Rotating your own key needs nobody: solver "user --regen" re-wraps the master key to your new key itself. Only a machine that cannot load the master key at all files a request.

That grant is a split, not a copy of the key. user-authorize (or key-split <you> directly) halves the master key: one half already sits on the host your instance runs on (/etc/euler/share.json), and the other is sealed to your public key and sent to you as a message. msg act on it runs key-reconstruct, which unwraps your half, puts the two together, proves the result, and writes ~/.euler/enc-key.json.

No single piece is enough: the half on the host is a random point that says nothing about the key, and the half you were sent needs your private key to open it and that host to complete it. On a machine away from the host there is no half to complete, so the grant is host-authorize instead — a sealed key by e-mail, which you install with host-unlock. The mathematics and both flows are in the Secret Sharing Guide.

Studying the solutions

Once you have master-key access the private files are plaintext in your working tree — just open them. Or use show N to view any problem's statement, notes, and results in the browser (the web front end at $EULER_BASE_URL); every show reuses one dedicated browser tab (solver-doc), so browsing through problems never piles up tabs.