Megillah
Megillah is a planning technology for sustained, nonlinear work conducted with AI collaborators. It keeps purposeful work — its framing, requirements, commentary, evidence, and history — in one durable record that any client can pick up cold.
The product argument and the domain that supports it are documented separately:
Using the CLI
Create a Workspace store with its first Project, then frame work into it:
$ megillah init --key MEG --name "Megillah" --description "Why the Project exists"
$ megillah frame "Draft the survey instrument" --project MEG --description "What done means here"
Framed MEG-1: Draft the survey instrument
$ megillah require MEG-2 MEG-1 # reads: MEG-2 requires MEG-1
By default the CLI uses ~/.megillah/megillah.sqlite3. Pass --store or set
MEGILLAH_STORE to use another Workspace store.
A Workspace holds its Projects side by side. project adds another to the
store and projects lists them:
$ megillah project --key TEM --name "Temenos"
$ megillah projects
MEG Megillah: Why the Project exists
TEM Temenos
A command acting in one Project as a whole names it by its key, with
--project or MEGILLAH_PROJECT: frame, available, search, tracks,
track, revise-track, dissolve-track, order-tracks, revise-project, and
artifact recording an Artifact for the Project itself. Naming none is
refused, even in a Workspace holding one Project, so a command that works
today keeps working when a second arrives. A command naming an Assignment,
Comment, or Artifact takes no Project: the identifier already says which
Project the record belongs to.
init also acquires the search models: about 219 MiB in total, with
its progress shown, into ~/.cache/megillah/search (or under
$XDG_CACHE_HOME). Without the network the Workspace is still created and
init says that search waits; serve or search acquires missing models the
next time one runs online.
init ends by saying what to do next: the serve command for the Workspace
it created; where the browser will be; and how to connect Claude Code, Codex,
and Claude Desktop to that instance (see Connecting an
agent). The command runs this installation's
megillah by its absolute path, as the agents do, since a megillah found
on PATH may be absent or another installation. It names the store by its
absolute path when it is not the default, and writes in any serving option
set in the environment (MEGILLAH_HOST, MEGILLAH_PORT,
MEGILLAH_ALLOWED_HOSTS), with the addresses following them, so it serves
the instance the agents are pointed at from any directory and any shell.
Search runs the models through ONNX Runtime, which ships for CPython 3.12 to 3.14 on glibc Linux (x86-64 and aarch64), macOS 14 or later on Apple silicon, and Windows (x86-64 and arm64). Megillah installs only where it does: not on Intel macOS or musl Linux.
The working loop revolves around three questions:
$ export MEGILLAH_PROJECT=MEG
$ megillah available # what can begin now?
$ megillah begin MEG-2 # begin it, and get situated
$ megillah inspect MEG-2 # how did we get here?
$ megillah search "survey evidence" # where has this come up before?
begin marks the Assignment active and prints its continuation context as
the Brief: its facts and description, then an index — prerequisites and
dependents by name, where prior collaboration lives, what commentary,
evidence, and activity exist, and how to pull each string. inspect is the full pull, bodies
included, with the Assignment's activity — the running account of what
happened to it — in recorded order. continue re-situates active work without
changing anything. Work ends with deliver, abandon (which, like
icebox, takes several Assignments as one change), or
a trip to the icebox (icebox, thaw); reopen takes a resolved
Assignment back. Framings are revised with reframe.
Work discovered mid-flight is placed into the shape of the plan by join and
insert, each landing its whole compound change or none of it. join puts an
Assignment into a Track — moving it if it already belongs to one — as a
prerequisite or a dependent of an Assignment already there; insert puts one
between two Assignments, replacing their Dependency with two. Both name the
Track: neither infers it from the Assignment it is placed against, so a
cross-Track placement stays a deliberate choice.
$ megillah join MEG-5 MEG-3 --role prerequisite --track Foundation
MEG-5 joined MEG-T1 as a prerequisite of MEG-3
$ megillah insert MEG-6 MEG-3 MEG-4 --track Foundation # between MEG-3 and MEG-4
MEG-6 inserted between MEG-3 and MEG-4 in MEG-T1
The rest of the graph-shaping board's operations are on the command line too,
each again whole or not at all. place puts one or more Assignments in a
Track, keeping their Dependencies, and relates them where they land when given
--anchor and --role or --between a Dependency's two ends; rehome moves
one Assignment, striking the Dependencies it can give up for the ones its
destination implies; detach removes every Dependency an Assignment takes part
in; and require-together adds an exact set of them. arrange and
order-tracks keep where cards sit and how Tracks stack, which is presentation
only.
$ megillah rehome MEG-7 --track Delivery --anchor MEG-4 --role dependent
Placed MEG-7 in MEG-T2, removing MEG-3 → MEG-7, adding MEG-4 → MEG-7
$ megillah order-tracks Delivery Foundation
Ordered the Tracks: MEG-T2, MEG-T1
Durable context accumulates alongside the graph: comment for commentary,
artifact for evidence, outputs, and locations such as a pull request or a
shared conversation (promote moves one to the Project while preserving its
origin). An Artifact's material is text given with --material or a file of
up to 10 MB given with --file, which Megillah then holds beside the store;
revise-artifact --file replaces it, and material writes a held file back
out:
$ megillah artifact MEG-7 --description "Wave 2 responses" --file wave2.csv
Recorded MEG-A3 on MEG-7
$ megillah material MEG-A3 --output ~/Downloads/wave2.csv
Wrote MEG-A3's file to /home/you/Downloads/wave2.csv
search spans visible and iceboxed, open and resolved Assignments. It searches
the current framing, Comments, Artifact text material, file names, and
locations, and activity. A held file's contents are not searched. Results
identify the matching source and show its excerpt; Assignment state is shown
but does not influence relevance. A title
the Assignment once carried is still found, because the account records it; a
description it once carried is not recorded anywhere and is not found.
Retrieval is hybrid and entirely local: the query as a literal phrase,
BM25 over SQLite's full-text index, and cosine similarity over a pinned,
quantized Nomic Embed Text v1.5 model, fused by reciprocal rank to select
up to 20 passages. A pinned MiniLM cross-encoder, run directly through
FastEmbed, then ranks these passages and withholds weak matches. For nonliteral
matches, a shorter displayed excerpt must independently clear a stricter
relevance cutoff, so incidental provenance alone cannot support admission.
Identical texts share inference; 20 candidates require at most 40 scored texts.
Literal phrase evidence remains discoverable beyond that bounded model input. A
literal phrase is the whole query in any case, neither beginning nor ending
inside a word or number: port is not found in "report", nor MEG-7 in
"MEG-70".
Results are a limited selection, not an exhaustive account of relevant work;
the relevance cutoff can still admit adjacent topics or miss useful records.
A query consisting of an identifier under the searched Project's key returns
only that record or exact mentions of it, without semantic substitutes. Queries
with other prefixes, such as ISO-9001 in Project MEG, use relevance ranking.
A Comment mentioning an identifier that has never been minted can therefore
match, attributed to the Assignment holding that Comment; it does not assert
that the mentioned record exists.
Excerpts quote source text, centering literal phrases. An activity excerpt
quotes the statement without its leading moment and Client unless the query
occurs literally in them; the model gives no attribution, so a semantic match
on the Client alone is quoted by its statement.
Record text never leaves the machine; model files are the only thing ever downloaded. The application offers retrieval the eligible records as values and accepts back only references it can verify against the same Workspace snapshot. Vectors for unchanged text are kept in a rebuildable cache beside the models, one index per Project, private to the user; deleting the cache loses no Workspace fact and costs one re-embedding and, for the models, new downloads. The index keeps only what the corpus it was last offered needs, so Projects sharing one would re-embed each other every time a User searched them in turn.
The first search loads the models and embeds any uncached text; later searches embed only new or changed text. Preparation currently happens during the search, not in the background. Restarting the listener reuses the vectors when the Workspace path, Project, model, and text are unchanged. Local embedding inference uses four threads and relevance inference uses two, so each listener has a bounded inference thread budget even on a machine with many cores.
Every protected open runs the store's defenses: an integrity check, a refusal
of any store whose schema this build does not use as it stands, and a small
ring of proved ordinary backups. No command migrates a store as a side effect:
megillah store migrate does, behind a proved backup, with every listener over
that store stopped. Record reads over HTTP open the store read-only
and run none of the defenses; the server takes one protected open before it
starts, and each mutation command takes its own. The megillah store group
(status, check, migrate, backup, backups, restore) is the on-demand
surface; Operating the Workspace store is the runbook.
Every command accepts --help.
Serving the Workspace
megillah serve is one Megillah instance: one process on one port, serving
the browser at its root and the records and commands that browser reads and
performs under /api on that same origin.
$ megillah serve
Warning: this HTTP boundary is unauthenticated. Anything that can reach it can read and write the Workspace record.
Serving /home/meg/.megillah/megillah.sqlite3 at http://127.0.0.1:8642
Serving the browser at http://127.0.0.1:8642
Reached at http://127.0.0.1:8642
The instance uses the store chosen at launch (--store or MEGILLAH_STORE)
and binds to the loopback interface by default. --host (or MEGILLAH_HOST)
chooses the interface it binds, a wildcard exposing it onward; --port (or
MEGILLAH_PORT) chooses its port; --allowed-host (or
MEGILLAH_ALLOWED_HOSTS, comma separated) names the hostnames it answers to
beyond loopback, such as a machine name on the local network or a tailnet;
and --no-browser serves the boundary alone. An instance bound to every
interface refuses to start until it is given its hostnames, because it
answers requests by the hostname they address and a wildcard names none.
The console gives such an instance's browser its loopback address, beside the
hostnames it answers to, since a request addressed to 0.0.0.0 is refused. An
allowed host is a hostname or IP address alone, as a request addressed to it
names it; one given with a scheme, port, or path could never be matched, so
it is refused at startup.
Unless the instance is guarded by a shared secret, nothing authenticates a request, so the console warns before the boundary can be reached.
Once it is answering, the instance follows each URL it answers at — the address it is bound to, or loopback when bound to every interface, and then each allowed host at its port — as a client would, and says whether it was reached there. A URL it was not reached at is named with the first stage that failed:
- DNS: the name does not resolve.
- connection: nothing accepts connections at any address the name resolves to.
- host admission: another server at that address refuses the host the request named. This instance answers every name it checks, so a refusal there comes from something else.
- application: what answered was not this instance answering from its Workspace — a failure, another program, Megillah at another release, or another Megillah instance altogether. The check knows its own requests arrived by a token only this process holds, so even an instance serving a copy of the same Workspace is told apart.
Not reached at http://megillah.local:8642 (DNS): megillah.local does not resolve: Name or service not known
The instance serves either way, since a name can come to resolve after a service starts. These checks are made from the machine the instance runs on, so they cannot see a firewall between it and another machine, or a name as another machine resolves it.
A request addressed to a host the instance does not answer to is refused
421, saying which host it named and that --allowed-host names it, so
reaching an instance by a name it was not given says what to add.
Because the page and the boundary it commands share one origin, a page is
admitted only when it came from that very origin — the same host and port.
A page from anywhere else is refused, including one on another port of the
same machine: sharing a machine earns a page nothing here. A caller that is
not a browser sends no origin at all, so the CLI, megillah mcp, and curl
reach every route. The browser and the listener serving it are one build of
one release, so nothing is negotiated between them; every answer still names
that release in X-Megillah-Version, as information.
The browser is built into the package when a distribution is built, so an installed Megillah carries it. A source checkout that has never built it — one with no Node installed — serves the boundary exactly as above and says on the console that the browser is not served.
One Workspace holds several Projects, so GET /api/projects names every
Project it holds and everything a Project answers is read under its key, as
JSON: /api/projects/<key> (that Project's facts and the Tracks in use),
/api/projects/<key>/assignments (its visible Assignments with derived
available and blocked conditions),
/api/projects/<key>/assignments/search?text=<query>&limit=<count>
(Assignment discovery, each result carrying the Assignment's current facts
and the bounded evidence supporting its rank),
/api/projects/<key>/dependencies (every edge), and
/api/projects/<key>/resolutions (resolved visible work at its effective
resolution moment). A key the Workspace does not hold answers 404.
/api/assignments/<id> answers one Assignment's full continuation record:
its identifier already names the Project that minted it, and a second
account of where it belongs could only disagree with the first.
Every response carries the release the listener loaded in
X-Megillah-Version. A caller may send that header too: it requires exact
release equality. After host, origin, and secret admission, a request naming
another release receives 412 Precondition Failed with release-mismatch
before dispatch, retry-key handling, or material storage. The request was not
performed. A caller sending no release header accepts the listener's release
and is served normally. The browser, being part of the same build as the
listener answering it, sends none; the CLI operates on its local store and
does not negotiate with a listener.
megillah mcp names its release on every request and checks /projects
before every write. A different release found by that read stops the write,
even when the listener predates request checks. The write itself still carries
the precondition, since a listener can restart between requests. MCP refuses
answers from another release before interpreting their bodies. A mismatched
write answered without HTTP 412 is
outcome-unknown: the caller must inspect the record before retrying and keep
the original idempotency key and arguments. HTTP 412 confirms refusal before
dispatch; release-mismatch on an MCP command therefore means the command
was not performed. Install the listener's release for the MCP server and
reconnect it. An answer without a release header remains not-megillah,
identifying a proxy or another service rather than a different Megillah build.
Record reads open the store read-only — they can never mutate or migrate a Workspace — and CLI writes made while the server runs appear in subsequent reads.
/api/projections/arrival answers the whole Workspace at once, since it is
what a person or collaborator arriving sees before choosing an undertaking:
every Project in the order it was created, read from one state of the store
and each carrying its own revision, with its framing and Tracks, the latest
activity of its visible work, its active work grouped by Track, every
Assignment that can begin, and how many are blocked. Each active Assignment
carries the moment it goes quiet, a week after its latest activity.
The planning projections of one Project are answered under
/api/projects/<key>/projections/, each stamped with that Project's
revision so a renderer can tell answers from one state apart from answers
from another — and so that a page open on one Project is not made stale by
work in another: temporal (resolution history beside the
open structure, with ?focus=<id> emphasizing one objective and its
prerequisites), structure (the connected bodies of visible work, with
?selected=<id> exposing one body in full), curation (one attention scope
with its three measures and, on each row, what applies to that Assignment
now, under ?scope=, ?track=, and ?selected=), placement (where work
is placed: the board of Tracks), and sequencing (the order in which work
must proceed, with ?history=true revealing the resolved work before it).
The placement projection carries the board's whole state at once — Tracks in
their kept order with visible and parked members, the Track order sequencing
derives as sequenced, the connected bodies of untracked work, every
connected body of visible work with the members iceboxing it would park as
parks (its undelivered work, null when that icebox would be refused), every visible Dependency with whether it can still be removed, each
card with whether it can be detached and, as icebox_detached, the
Dependencies iceboxing it with its removable ones struck would strike (null
when that icebox would be refused), the kept card positions, and what a
newly framed Assignment could relate to. Given a subject, ?assignment=<id>
for one Assignment, parked or not, or ?body=<id> for the untracked body
holding an Assignment, it also answers targets: every accepting spot and
every refusing card side. Each target has at, offers, and conflicts.
An offer is an exact command document with the change it makes, every
Dependency it adds saying whether it joins two Tracks. Offers and refusal
facts come from rehearsing the shaping operations themselves against the
current record, so what the board offers is what the boundary accepts at
that revision. When no change accepts at a card side, its offers is empty
and conflicts names the distinct refusing facts in the same
{record, fact, detail} form as a command refusal. An accepting target has
empty conflicts; other kinds of spot with no accepted change are absent.
The sequencing projection carries every Track as tracks, in sequencing's
order, each with counts of its visible members and those still open,
and the same counts for trackless work as trackless; neither counts the
Icebox or changes with history. Each Track's dissolution is null when the
browser offers none, an empty list when the Track is empty, or the exact
open, iceboxed Assignment records whose release must be confirmed. These
facts are independent of which rows or history the page shows.
Visible open work and every abandoned Assignment one of them requires make
up the order, as assignments. Asked
for with ?history=true, the rest of the visible resolved work comes as
history, which is otherwise null. Each Assignment has its position: in
the order, the length of its longest chain of prerequisites within the order
in any Track; in history, a negative place counted back from where the order
begins, -1 for work nothing in history requires and a further place back
for each Assignment in the longest chain of its dependents within history.
Revealing history moves nothing in the order and leaves tracks in the same
order. Each also has its body, the least-numbered member of its connected
body among the work carried, and, as a board card does, its operations, the
Dependencies at it that are removable, and whether it is detachable. The
dependencies are those among the work carried, each saying whether it is
unsatisfiable, which open work waiting on an abandoned prerequisite is;
every other Dependency is left out.
The optional ?focus=<id> names an Assignment in the order or revealed history.
It combines with history=true. The answer's
focus identifies that objective and emphasized names it and its transitive
prerequisites among the cards shown. Reach is computed through the full graph;
focus changes neither the sequence's contents nor its positions. An objective
outside the sequence answers not-found.
The optional ?assignment=<id> names an Assignment being carried, as it does
for the placement projection, and the answer's subject and targets take
the same form. Sequencing carries one Assignment at a time, so it names no
body.
The continuation record at /api/assignments/<id> likewise states what applies
to the Assignment now: operations, the lifecycle and framing operations
that would not be refused, revisable, the framing fields a reframe may
revise, removable, the Dependencies at the Assignment an unrequire
would strike, and withheld, each lifecycle operation that would be refused
with the conflicts refusing it in the same {record, fact, detail} form as a
command refusal, so an inspecting surface offers exactly what applies and
explains the rest in the rules' own words. Curation rows carry the same
operations and withheld. The record's prerequisites and dependents also
carry their own availability and blocking. The placement projection does the
same for framing choices, parked members, and outside endpoints of crossing
Dependencies, keeping state readable wherever the browser identifies the
work.
Discovery uses the same retrieval as megillah search. serve acquires the
model before it listens if it is not already there, so the download happens
at the console rather than inside a request; without the network it says
so and serves anyway. Search is the only route that depends on the model:
every other record stays readable while it is absent. A search made without
it answers 503 under the unavailable code — the capability is absent,
which is not the same as the 404 that answers a request for a record that
does not exist. One search is answered at a time, so a search arriving while
another is underway is turned away as busy, also 503; that one is worth
retrying rather than a sign anything is wrong. A query matching nothing is a
success with an empty result list.
Mutations are performed by POST /api/commands/<name>, whatever Project
they act in, one command per application operation under its CLI name:
frame, reframe, begin, continue, set-aside, deliver, abandon,
reopen, icebox, thaw,
require, unrequire, require-together, detach, join, insert,
place, rehome, arrange, order-tracks, comment, revise-comment,
artifact, revise-artifact, promote, track, revise-track,
dissolve-track, project, and revise-project.
A command document is JSON with two members —
attribution naming the client, and arguments carrying the operation's
facts:
$ curl -s http://127.0.0.1:8642/api/commands/frame \
-H 'Content-Type: application/json' \
-d '{"attribution": {"client": "curl"},
"arguments": {"project": "MEG",
"title": "Draft the survey instrument"}}'
{"assignment": {"id": "MEG-1", ...}}
A command whose arguments name no record already belonging to a Project
carries a project argument naming one by its key: frame, track,
revise-track, dissolve-track, order-tracks, revise-project, and
artifact when it names no owning Assignment. The project command instead
gives the key a further Project will carry, with its name and optional
description, and refuses a key the Workspace already holds in any case.
Every other command reads its Project from the Assignment, Comment, Artifact,
or Track its arguments already name, and refuses a project beside it as an
unknown key.
dissolve-track requires release: true whenever the Track holds any
Assignment, including open iceboxed work; without it the command answers a
409 refusal. Release makes every member trackless, preserving its state,
Dependencies, and history. An empty Track needs no release confirmation.
The CLI expresses the same intent with dissolve-track --release.
The browser includes confirmed_iceboxed, the list of Assignment IDs shown in
its confirmation. With that argument, release succeeds only if those are still
exactly the Track's members and every one is open and iceboxed; a mismatch
answers 409 and changes nothing.
An Artifact's material is a string of text, or a held file offered as
{"file": <digest>, "name": ..., "media_type": ...}. A file is held first, by
POST /api/material with its bytes as the body, which answers the file
digest and size to offer; a body over 10 MB answers 413 before it is read,
and holding the same bytes again changes nothing. GET /api/artifacts/<id>/material retrieves an Artifact's held file as a download
under its recorded name and media type; it is never served as a page.
Revisable fields follow presence: an absent key leaves the field alone, and
a present key — null included, for clearable fields — revises to that
value. A performed command answers 200 with the changed record (begin
and continue answer the continuation record); a domain refusal answers
409 with the facts in conflict; transport failures are structured JSON
with 400, 404, 405, 411, 413, 415, or 503. Each command runs in one
transaction on its own protected connection, exactly as a CLI invocation
would.
Every performed command also answers reversal: the one command that would
restore what the change altered, or null when no single operation would.
Removing a requirement is reversed by requiring again, a revision — of an
Assignment's framing, a Track, a Comment, an Artifact, the Project — by the
previous values of the fields it changed, parking one inactive Assignment
by thawing it, abandoning or delivering one inactive Assignment by reopening
it. Iceboxing or abandoning active work ends its pursuit, which thawing or
reopening does not resume, and delivering iceboxed work thaws it, which
reopening does not undo, so those answer null; so do abandoning several
Assignments at once, reopening (delivering again would
resolve the work at a new moment), requiring or unrequiring several
Dependencies at once, dissolving a Track (a recreated one is another Track),
creating a Project (nothing removes one), and anything recorded rather than
altered. A renderer offers to take a change back exactly when the answer
names how.
A response can be lost after its command has already performed, leaving a
client unable to tell whether sending it again would repeat the work. An
Idempotency-Key settles that: a retry bearing the same key and body is
answered with the recorded response rather than performed a second time. It
matters most for frame, comment, and artifact, where a blind retry would
mint a second Assignment, Comment, or Artifact, and for the project command,
where it would be refused because the first attempt already created the
Project; the browser sends every command under one.
Recordings live in server memory, bounded to the most recent 1024 keyed
commands of one serving lifetime — far wider than any transport retry — and
beyond that bound a retried key performs afresh.
The two compound commands answer with every record they touched, so a
renderer can redraw the change without reading the graph again. join
answers assignment and the dependency it added; insert answers
assignment, the removed edge, and the two added edges that replaced
it, in that order. Either is performed whole or not at all: a refusal leaves
the membership and every edge exactly as they stood.
These two want the key even though neither can duplicate anything. A
repeated insert answers 409 — the Dependency it would replace is already
gone, and the two it would add are already recorded — and a repeated join
answers the same way, its Assignment having already joined the Track. Those
are ordinary conflicting facts, indistinguishable from a refusal of a request
that never landed, so without a key the only way to learn which happened is
to read the graph back.
A browser page may read the boundary only from the very origin its request
is addressed to — the same host and port — which is where the instance
serves its own pages. A page from anywhere else is refused 403 outright
rather than merely denied a cross-origin grant, so it cannot make the server
spend an embedding pass on an answer it would never be allowed to read. No
cross-origin grant is made to anyone, since every page the boundary answers
is its own. Commands are closed to pages from any other origin for a second
reason: they require the application/json media type, which a page cannot
send across origins without a preflight this boundary does not answer. A
development server proxying /api presents the boundary's own origin in the
page's place, so no preflight arises there either. A request addressed to a
foreign host — the mark of a DNS-rebinding page — is refused whatever its
origin.
Beyond this machine
On a tailnet or a private network, bind every interface and name the hostnames the instance is reached by:
$ megillah serve --host 0.0.0.0 --allowed-host megillah --allowed-host megillah.example.ts.net
Plain HTTP carries the record there. A tailnet encrypts it between machines; an ordinary local network does not, and anything on it can read and write the Workspace — a guarded instance's too, since its shared secret crosses that network as readably as the record.
Anywhere else, put a reverse proxy in front of the instance to serve HTTPS, and leave the instance bound to loopback behind it. Megillah serves no TLS of its own. The allowed host is the name the proxy serves:
$ megillah serve --allowed-host megillah.example.com
The proxy must pass each request's Host header through unchanged, port
included. The boundary admits a browser page only from the very origin its
request is addressed to, and it reads that address from Host, not from any
X-Forwarded-* header. A proxy that replaces Host with the listener's own
address leaves every page looking foreign: the browser shows the Workspace,
but its commands are refused 403, with a detail naming the origin the page
came from and the host the request reached.
Caddy passes Host through by default and obtains the certificate itself:
megillah.example.com {
reverse_proxy 127.0.0.1:8642
}
nginx replaces Host unless told otherwise. $http_host keeps the port a
browser names at a nonstandard port, which $host drops:
server {
listen 443 ssl;
server_name megillah.example.com;
ssl_certificate /etc/letsencrypt/live/megillah.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/megillah.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8642;
proxy_set_header Host $http_host;
}
}
Behind a proxy, the startup check reaches the instance at loopback, and reports the public name at the listener's own port unreached at the connection stage: that name resolves to where the proxy listens, and the listener accepts connections at loopback alone. That line is expected here; the public URL is checked by opening it.
An agent elsewhere reaches the instance through the proxy, at the boundary's prefix:
$ megillah mcp --listener-url https://megillah.example.com/api
When the proxy answers but the instance behind it does not, megillah mcp
says that something other than Megillah answered, naming the status the
proxy gave.
HTTPS keeps the record private in transit; it does not decide who may reach it. Unless the instance is guarded by a shared secret, anything that can reach the public name can read and write the Workspace.
Guarding the boundary with a shared secret
An instance reached over a network can require a shared secret. Set
MEGILLAH_SHARED_SECRET in the environment megillah serve starts in, and
every request for the record or a command, under /api, must present it;
anything else is refused 401 with an unauthorized failure, before its
route is even looked up. The browser's page and assets are answered without
it: they hold no record, and the browser has to load them to ask.
$ export MEGILLAH_SHARED_SECRET="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
$ megillah serve --host 0.0.0.0 --allowed-host megillah.example.ts.net
Guarded: this HTTP boundary answers only requests presenting its shared secret. Whoever holds the secret can read and write the Workspace record; it names no one, and it crosses the network readable unless HTTPS or a private network such as a tailnet carries it.
The secret is read from the environment alone, never an option, so it appears in no process listing or shell history; surrounding whitespace, such as the newline a secret file ends in, is not part of it. A variable set to nothing is refused rather than read as no secret, since that usually means the secret meant for it went missing. Nothing limits how often a secret may be guessed, so make it long and random, as above.
A caller that is not a browser presents it as a bearer credential:
$ curl -H "Authorization: Bearer $MEGILLAH_SHARED_SECRET" http://127.0.0.1:8642/api/projects
The browser asks for it once, when the instance first refuses one of its
requests, and holds that request and any other until the instance accepts
it; nothing typed into the page is lost meanwhile. The page presents the
secret to POST /api/session, and the instance answers with a session
cookie derived from the secret rather than holding it: sent only to
/api, never readable by a script on the page, never sent with a request
another site started, and marked Secure when the page was reached over
HTTPS. Every tab shares it until the browsing session ends. The cookie is
named for its secret, so instances on one host guarded by different secrets
keep their sessions apart even though a browser keeps cookies by host alone.
Change the secret and restart the instance, and each open page simply asks
again. Every answer a guarded instance gives is marked Cache-Control: no-store, so no cache in between replays one to a caller without the
secret.
megillah mcp presents the secret it finds in its own environment, so give
each Client's server the same variable (claude mcp add -e,
codex mcp add --env, or an env entry in a JSON configuration). A server
given none, or the wrong one, says which when the instance refuses it.
megillah init run with the variable set says as much beside each Client's
configuration, and never prints the secret itself.
The secret protects the boundary; it is not identity. It names no one, one secret is shared by everyone and everything that holds it, and whoever holds it can read and write the whole Workspace, just as whatever reaches an unguarded instance can. Megillah still does not represent who a person is.
The startup check presents the secret only once its probe has shown that an address reaches this very instance, since a name may resolve to another server and the probe travels over plain HTTP.
Connecting an agent
megillah mcp serves the Workspace to MCP clients over standard input and
output. It opens no store: it reaches a listener over HTTP, so it can run on
a laptop against an instance serving somewhere else.
$ megillah mcp --listener-url http://127.0.0.1:8642/api
MEGILLAH_LISTENER_URL stands in for the option; the default is where a
local instance answers, its loopback address and the boundary's prefix.
An instance guarded by a shared secret
also needs that secret in the server's MEGILLAH_SHARED_SECRET.
The tools are the Workspace's own vocabulary: listing its Projects, reading a Project and its Tracks, listing and searching Assignments, retrieving one Assignment's continuation context, framing and reframing work, the whole Assignment lifecycle, Dependencies, Comments, Artifacts, and Tracks. The server decides nothing: each tool performs the same read or command an interface would, and the listener remains the authority for what the record says and for every refusal. A domain refusal comes back as an ordinary result carrying the conflicting facts, so a collaborator reads them and chooses again; only a listener that could not answer at all is an error. Every contribution is attributed to the Client the connection negotiated, so the record says which collaborator made it.
Search calls allow 30 seconds to read the listener's response; other reads, connecting, and sending a request retain a five-second timeout. Read and write timeouts are reported separately from an unreachable listener: work may still be running after the caller stops waiting. If a command times out, its outcome is unknown; retry it only with the same arguments and the idempotency key returned in the timeout result, so an already-applied command is not repeated.
A collaborator sends a file as an Artifact's material whole, base64 encoded,
and the server holds it before sending the command that names it.
retrieve_artifact_material returns a held file as an image, text, or bytes;
continuation context describes held files and names that tool rather than
carrying them. A collaborator with a shell on the Workspace's machine can use
megillah artifact --file instead of sending a large file through a call.
A tool acting in one Project as a whole names it, as the boundary does: every
read of a Project, framing, creating, revising, ordering, and dissolving Tracks,
revising the Project, and recording a Project Artifact take a required
project key, and list_projects names the keys the Workspace holds. A tool
naming an Assignment, Comment, or Artifact takes no key, since its identifier
already names the Project, and create_project names none but gives the key a
new Project will carry. The server never picks a Project for a
call — not even a Workspace's only one — so a call keeps meaning the same
thing when a second Project arrives.
Each answer carries the release the listener reported, and a server built for another release refuses it rather than reading a document whose meaning may have moved — so keep the two ends current together.
megillah init prints each Client's configuration for the instance it
describes. Each names this installation's megillah by its absolute path,
since a Client need not start servers with the shell's PATH and Claude
Desktop, launched outside any shell, does not; and each names that instance's
listener outright, so a MEGILLAH_LISTENER_URL in the Client's environment
cannot point it elsewhere. Claude Code and Codex add a server from the command
line, Claude Code at user scope so that every project reaches the instance:
$ claude mcp add --scope user megillah -- /home/meg/.local/bin/megillah mcp --listener-url http://127.0.0.1:8642/api
$ codex mcp add megillah -- /home/meg/.local/bin/megillah mcp --listener-url http://127.0.0.1:8642/api
Claude Desktop reads claude_desktop_config.json, opened from Settings >
Developer > Edit Config, and takes a new server when restarted:
{
"mcpServers": {
"megillah": {
"command": "/home/meg/.local/bin/megillah",
"args": ["mcp", "--listener-url", "http://127.0.0.1:8642/api"]
}
}
}
A project's configuration can instead name megillah alone, relying on the
PATH of whoever opens it. Claude Code reads a project .mcp.json:
{
"mcpServers": {
"megillah": {
"command": "megillah",
"args": ["mcp"],
"env": { "MEGILLAH_LISTENER_URL": "http://127.0.0.1:8642/api" }
}
}
}
Codex reads the same configuration from ~/.codex/config.toml:
[mcp_servers.megillah]
command = "megillah"
args = ["mcp"]
env = { MEGILLAH_LISTENER_URL = "http://127.0.0.1:8642/api" }
Attribution and provenance
Operations record the contributing Client, read from options or the environment:
| Variable | Meaning |
|---|---|
MEGILLAH_STORE |
Path to the Workspace store |
MEGILLAH_CLIENT |
The Client recorded as provenance |
MEGILLAH_PROJECT |
The Project a command acting in one Project as a whole acts in |
MEGILLAH_HOST |
Interface megillah serve binds |
MEGILLAH_PORT |
Port megillah serve listens on |
MEGILLAH_ALLOWED_HOSTS |
Hostnames the instance answers to, comma separated |
MEGILLAH_LISTENER_URL |
Where megillah mcp reaches the boundary |
MEGILLAH_SHARED_SECRET |
The secret guarding the boundary, which megillah mcp presents |
megillah mcp takes no session Client. One server serves many conversations
across many Assignments, so each contribution is attributed to the Client its
connection negotiated. Nor does it take MEGILLAH_PROJECT: a tool acting in
one Project as a whole names it. Starting it with that configured says so
rather than quietly ignoring it.
Versioning
Megillah's release is derived, not written down. It is read from the commit a
checkout is on: the UTC day that commit was made, and where it falls among
that day's commits — 2026.9.10 for the first, 2026.9.10.1 for the next,
and so on. The build stamps it into the distribution's metadata, and installed
processes report that.
Every merge adds a commit and changes the release, without a manual version edit. An installed distribution keeps the release stamped at build time, even when its source checkout advances. Reinstalling updates that metadata.
Building needs the whole history. A shallow checkout holds only the commits it was given, so it would count fewer of the day's commits than exist and derive a release another commit already carries; the build refuses that rather than answer it. Continuous integration must check out with full depth, and a source archive carries the release already stamped into it, so building from one needs no history at all.
uv run python -m megillah.versioning, run in a checkout, prints the release
that checkout derives.
Publishing a release
Every merge to main whose Verify run passes is published to PyPI under its
release. Nothing is tagged or chosen by hand. Verify builds the source archive
and wheel, installs the wheel alone, and serves the browser from it. Once every
job has passed on main, it uploads that same wheel. The source archive is
built and checked but not published, which keeps each release small.
Releases are unique along main's first-parent line: each commit there
descends from every earlier one, so a later commit made the same day always
counts more of that day's commits. Every commit on that line gets its own
Verify run, and runs on main are never cancelled, so no merge is skipped. A
merge whose run fails is not published, and the next one that passes is.
The upload uses PyPI's trusted publishing: PyPI accepts a job running in this
repository's pypi environment by its GitHub identity, so no credential is
stored. That environment admits only main, so pull requests and other
branches never publish. A published release cannot be replaced, only yanked.
This release identifies artifacts built from this repository. It is not the Workspace schema version and not an outside-process protocol version: the MCP protocol revisions the server speaks are negotiated with each client and move on their own schedule.
The browser
The planning browser lives in browser/: a Vite and React application over
the boundary above, owning interaction and visual projection and nothing
else. It reads the planning projections and the continuation record, and it
sends the command envelope; what a card may become, which
operations apply to an Assignment, and what takes a change back are all read
from the boundary rather than decided on the page. Its surfaces:
/, where the browser opens, shows the Workspace's arrival: its Projects side by side, in the order they were created. Each shows its description, its active work grouped by Track (a Track opens to name its Assignments, blocked work marked, and quiet work marked), every Assignment that can begin, and how much open work waits on other work; each Assignment leads to its card in Placement. New Project asks for its name first and suggests an editable key: the first three characters of a single word, or the initials of several words, in uppercase ASCII letters and digits. The suggestion follows the name until the key is edited; a name without an ASCII abbreviation needs a manually entered key. Creation takes that key, name, and optional description, and the new Project joins the page; the listener alone decides whether a key can be used, and its refusal is explained in the dialog. Each Project's views live under/projects/<key>/, and an address naming no Project leads here. The Project's name in every view's header opens a menu of the Projects with their active and available counts: choosing one opens the same view there, without the focus, and All Projects returns to/. Moving between them replaces the page rather than reloading it./projects/<key>/sequencing, where a Project opens, shows open work in the order it must proceed: each Track a row spanning from its first work to its last, each Assignment in the column its longest chain of prerequisites puts it, read along an axis from prerequisites to dependents, with abandoned prerequisites marked unsatisfiable. Every Dependency is a connector turning at right angles through the gaps between cards, in a lane of its own where gaps are shared. Nothing is placed by hand there: a card opens its record, and carrying one, by pointer or with Space and the arrow keys, to another card's side, a Dependency's handle, or another Track's row proposes the change carrying it there in Placement would, lighting the destinations the listener accepts, explaining a refusing side, and confirming a plural or cross-Track result as Placement does. A trackless card is carried on its own; released beside work in a Track it is offered joining that Track or staying trackless, and on a Dependency between two Tracks it is placed in the one chosen. Released anywhere else it changes nothing, and while it is carried the notice says that positions follow Dependencies. A card's menu shows it in Placement and offers, as Placement's does, Icebox, Remove Dependencies and icebox, Remove all Dependencies and Abandon where the listener accepts them. A carry, a menu and a change in flight exclude one another, and automatic refresh waits for them. After a change the order is recomputed, the card glides to where it now stands and pulses, or the notice says it has left Sequencing, with the change's reversal offered. A minimap framed on the work keeps the whole order reachable at card size, and each row's name stays in view while its row does. Scrolling may carry the view past any edge of the work, so its first columns can be brought to the middle of a wide screen, but stops while a strip of the drawing is still in view, clear of the minimap; a move that leaves no card in view outside the minimap glides back to the nearest one. The sidebar creates, renames, and dissolves Tracks. New Track, or the finder's offer for a name not already held, opens the creation dialog; searching alone creates nothing. A new empty Track appears in the finder. Rename is a separate control beside each Track, including hidden ones. Dissolution follows Placement's offers: empty Tracks dissolve immediately, and Tracks holding only open, iceboxed Assignments ask before releasing the named members trackless, still iceboxed. A concurrent membership or state change refuses that confirmation without changing the record. A Track's name brings its row into view, a checkbox shows or hides its row (the row of trackless work too), and Show all, Show unresolved, and Hide all choose every row at once, Show unresolved keeping the rows that hold work in the order. Hiding a row moves no other work, and the sidebar names each Dependency reaching into a hidden row. Show resolved Assignments (/projects/<key>/sequencing?history) reveals the rest of the resolved work as history in the columns before a boundary marking where the order begins, moving nothing in the order; going to a Track whose work is all resolved reveals it. Focus objective in a card's menu opens/projects/<key>/sequencing/<id>: that Assignment and its transitive prerequisites stand out while other work remains visible as context, in the same positions. The objective is named above the canvas, with Clear focus to restore ordinary emphasis. Focus survives opening and closing records, reloads, and browser navigation. Focus changes and clearing retain the history choice; hiding history clears a historical objective with an explanation. An objective link reveals its row if hidden. A successful edit in the record brings its card into view and pulses it after the record closes; if it leaves Sequencing, a notice explains its departure and its objective focus clears./projects/<key>/placementis the graph-shaping board: one row per Track beside a tray of connected untracked structures;/projects/<key>/tracksstill leads there. An address naming an Assignment rings its card, or the untracked structure holding it, with the pulse a changed card gets. Carrying a card or structure to a card edge, a Dependency midpoint, or Track whitespace previews the exact change the listener offered there and asks for confirmation when the result is plural or crosses Tracks. Pointer hover and keyboard navigation explain a refusing card side at that destination. Releasing over that side or the nearby area treated as that side (its targeting halo) changes nothing and keeps the reason visible, without falling through to placement in the row. Track whitespace outside card halos keeps its placement and arrangement behavior. Every carry has a keyboard equivalent. The board starts with visible open work: available, active, and blocked. Show resolved Assignments reveals delivered and abandoned cards within the shown Tracks and in trackless work, without changing which rows appear or returning iceboxed Assignments. Filtering preserves records and saved arrangements; boundary notices name hidden Dependency endpoints, including abandoned prerequisites that still block open work. Tidy releases saved positions and derives an orderly layout for the whole board, including work hidden by the current view. Sort Tracks by dependency keeps the Track order sequencing derives, and is unavailable while the Tracks already stand in it. The Track finder creates Tracks, navigates to a Track when its name is clicked, and shows or hides its row through a separate checkbox. Navigating reveals a hidden Track; choosing the same name again brings its row back into view. All Tracks remain findable, including fully resolved, empty, and manually hidden Tracks. Show all reveals every row, Show unresolved restores the rows with visible open work, and Hide all hides every row. These actions apply across the Project even when the finder contains a search, reset individual row choices, and leave Show resolved Assignments unchanged. Choosing a Track also leaves that checkbox unchanged; opening a specific resolved Assignment reveals both its card and its Track. A dissolvable Track has a dissolve control in the finder as well as on its row, usable even while the row is hidden. An empty Track dissolves immediately; one holding only open, iceboxed Assignments asks before releasing them trackless, still in the Icebox. Each Track shows a muted total followed by a badge containing the open count, with one tooltip explaining both; counts exclude the icebox and do not change when resolved cards are hidden. A Track's row notes how much of its open work is in the icebox; resolved work parked there is left to the Icebox scope. Newly created Tracks appear immediately. When the resolved-work filter excludes members of a connected body, reveal those members and their Tracks before carrying the whole body, and before iceboxing it when they include work it would park. Iceboxing a body parks its undelivered work and leaves its delivered work in view. Whole-body actions retain their full membership across Tracks the User deliberately hides. A Track's row renames, reorders, folds, and dissolves it in place. The + beside its title controls opens Create Assignment with that Track selected; the Track can still be changed in the dialog./projects/<key>/assignments/{available,active,blocked,unresolved,all,icebox}are the curation scopes with their measures. Outside the Icebox the selection is iceboxed as one atomic command; in the Icebox the selection is abandoned as one atomic command, and one selected Assignment at a time is thawed. A single selected Assignment is offered only what applies to it, and an action withheld from it stays visible but disabled, its tooltip giving the listener's reason. Several selected Assignments are offered the icebox whole, for the listener to accept or refuse together; in the Icebox they are offered abandonment when the listener offers it to each, and otherwise the tooltip gives its reason for each it is withheld from, while Thaw says it takes one Assignment at a time. Icebox has its own link in the browser header, opening the Icebox scope and marking Icebox as the current destination./projects/<key>/timelinecombines resolution history with the unresolved dependency structure and supports objective focus and Track filtering. It is absent from primary navigation but remains reachable by URL and Assignment-row shortcuts.- Opening an Assignment exposes its continuation record and sends individual commands for framing, relationships, lifecycle, Comments, and Artifacts. Comments and Artifacts appear as separate feeds in recorded order. Artifacts start compact, with long descriptions and stored material expanded on demand; locations and revision actions remain reachable. Each Artifact opens independently and keeps its reading state through refresh, promotion, and revision editing while the Assignment stays open. Promoted and selected Project Artifacts use the same presentation. An Artifact takes a file of up to 10 MB as its material in place of text, downloads it, and replaces it in a revision; a larger file is refused where it is chosen. Actions puts Icebox or Thaw first, followed by Begin or Set aside, Deliver and Abandon, or Reopen as the record permits. Each action explains its effect. Continue is omitted from the open record, which already shows the continuation context and refreshes automatically. Actions keeps Deliver visible for blocked work and Icebox for visible work that is not delivered. When the listener withholds either, it is disabled, and hovering or focusing it shows the listener's reason: the undelivered prerequisites, or the visible open work that prevents iceboxing it alone.
Tracks share distinct colors across Placement, Sequencing, and the timeline. Renaming, reordering, hiding, or revealing a Track keeps its color; newly created Tracks receive unused colors. Colors are derived from the Project's full Track roster, so dissolving a Track may reassign colors. Trackless work stays neutral. In Sequencing, within-Track Dependencies are grey and crossings use their source Track's color (the destination's when the source is trackless); unsatisfiable Dependencies remain red and dashed.
The Assignment table can combine several Tracks or states: choices within one filter match any selected value, while separate filters narrow each other within the current scope. Applied filters can be removed individually or cleared together. Filtering a selected Assignment out also deselects it. Switching scopes keeps the previous scope's controls and rows together until the new scope is read successfully. If that read fails, the previous scope remains visible with the refresh error; Refresh retries the requested scope.
The table remembers its sort columns, directions, and precedence in this browser for each Project, across all Assignment scopes including the Icebox. The choice survives leaving the view, reloading, and reopening the browser; clearing the sort is remembered too. Without a saved preference, the table starts with Downstream descending. If browser storage is unavailable, sorting still works while the table remains open.
Every planning view has a Search records bar. Type an ID, title, or phrase and press Enter or choose Search to search the current Project's full record, including resolved and iceboxed work and work hidden by the view's filters. Results retain the search engine's order and show state and matching source excerpts. Choose one by clicking or with the arrow keys and Enter to open its Assignment record over the current view. Closing the record returns focus to search and keeps the view, filters, scroll position, and camera. Escape dismisses the results; Clear removes the query and results.
Prerequisite and dependent pickers find IDs and titles as you type; 10
puts MEG-10 ahead of partial ID matches. Search full records also finds
durable context such as Comments and Artifacts, with matching excerpts.
Only relationships the listener offers can be selected. Pickers include eligible
matches from up to 50 ranked search results; ID/title lookup remains usable
when that search is unavailable.
Every performed command is followed by an authoritative projection refresh; the browser never reproduces a domain decision optimistically. While visible, the browser also refreshes every three seconds after the previous read finishes, so contributions through other Clients appear without a manual reload. Hidden pages pause these reads and catch up when shown again; focus and network recovery also prompt a refresh. Unchanged revisions preserve the current scene, and automatic refresh waits for board gestures and dialogs to finish. Assignment drafts survive refreshed records, and failed reads retain the last successful view while subsequent reads retry.
The browser is built into the distribution alongside the listener that serves it. Installing that distribution supplies both, so the browser needs no runtime version handshake with the listener.
$ cd browser
$ npm ci
$ npm run build # into src/megillah/web/browser, for the listener to serve
$ npm run dev # a development server proxying /api to the listener
$ npm run typecheck && npm run lint
$ npm test # fast unit suites
$ npm run test:e2e -- inspector # workflows for the affected surface
Building a wheel builds the browser too, so this is only for working on it; an installed Megillah already carries one.
The development server proxies /api through to a listener on loopback,
MEGILLAH_API_PORT choosing the listener's port and MEGILLAH_DEV_PORT its
own. Since the listener admits a page only from the origin a request is
addressed to, the proxy presents the listener's own origin in place of the
development server's — a trusted reverse proxy on the same machine, saying so.
It vouches for exactly the origins it serves pages at, its own port on
loopback or on a hostname it was told to answer to, and passes anything else
through for the listener to refuse: a page on another port of this machine is
no more entitled through the proxy than at the listener itself. It answers to
loopback alone unless MEGILLAH_BROWSER_HOSTS names more.
The browser is tested in three layers. Unit suites state the page's
decisions over plain values — when it reads and what it adopts, which search
answer wins, where a camera goes, what command a form sends — and run in
seconds. Browser workflows drive the built page, served from disk, against
answers recorded from the listener: tests/boundary/browser_answers.py
performs each scenario in browser/tests/recorded through the real HTTP
boundary and records every document the page reads, its --check fails when
a recording no longer matches what the listener serves, and the page's own
document schemas parse every recording. No listener, store, or model is started, and
the page is refreshed through its own wake events or a virtual clock, so no
workflow's verdict depends on elapsed time. Each workflow checks one way the
page is wired to its decisions or its boundary: it loads the page once at the
state it needs, performs the least interaction that exercises the wiring, and
observes that one result, so it takes a second or two. The decisions themselves,
and their combinations, belong to the unit suites; a slow workflow is carrying
code that is not its subject, or its subject is slow. The bundle is rebuilt
before the run when it is stale. Rendered layout, and what only the real
listener, store, and search model can show, are checked on demand by agents
when a change calls for them, as the validate-visual-change and
exercise-live-browser skills describe.
Choose local checks using the testing guide. npm test
runs only unit suites; Playwright workflows are selected explicitly by file
or test name. CI runs every workflow, and npm run test:all runs both browser
test layers locally when a complete run is needed.
License
Megillah is free software under the GNU Affero General Public License, version 3 or later. See LICENSE. Anyone may run, study, change, and share it; anyone who offers a modified Megillah to others over a network must offer them its source under the same terms.
Development
$ uv sync --locked --extra dev
$ uv run --no-sync ruff check .
$ uv run --no-sync ruff format --check .
$ uv run --no-sync mypy
$ uv run --no-sync pytest
The browser's own checks run from browser/, as above. The Python suite
does not need the bundle built: the tests of serving it use a stand-in
bundle of a few files, and the command-line tests that need a real bundle
are skipped until one is built. Search integration tests also skip locally
until megillah init or megillah serve has acquired the pinned models.
The Verify workflow runs on pull requests and
pushes to main, with separate Python and browser checks. It installs from
uv.lock and browser/package-lock.json, prepares the pinned search models
for the Python suite's search coverage, and runs the browser's typecheck, lint,
unit suites, build, and workflows against recorded answers. The browser check
also runs the Python CLI serving tests with the bundle present, and checks
that every recording still matches what the listener serves. Failed browser
workflows retain their report and traces as browser-test-results for seven
days. Whether these checks are required for merging is configured
separately through branch protection.
The suites under tests/ are placed by the level of abstraction they
exercise: unit/ states promises over values and pure interactions,
application/ exercises operations against a real store, boundary/ drives
the CLI, the served Workspace, and the MCP server in process as black boxes,
and composition/ proves the real sockets, processes, builds, and Git once
each. tests/scenarios/ enacts one story per module and shows what the record
can answer afterward. While developing, uv run --no-sync pytest tests/unit
answers in seconds; add the level that owns what is changing. The
testing guide governs local check selection and reuse of
passing results through review and publication. Wall-clock budgets are marked
assay and run only on request (uv run --no-sync pytest -m assay).
When writing or reviewing Python or browser tests, follow the test-naming convention.
Release files for megillah 2026.9.16.91
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| megillah-2026.9.16.91-py3-none-any.whl | Python 3 | none | any | Details |
Release files / megillah-2026.9.16.91-py3-none-any.whl
| Download URL | megillah-2026.9.16.91-py3-none-any.whl |
|---|---|
| Size | 970.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a15f607ecaef55df5f2b34abd2b44bbef8235dde9d620f228adbb60a8d9bb0ee
|
|
BLAKE2b-256 checksum How to use checksums |
03ef956acbc5329f8cfd9c79aed1dbaf84fb0b233732ee3ef28cf09ddb9ef225
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|