You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every tool flake.nix puts on PATH, with its script and the devkit.just recipe that wraps it.
Each one is also a flake app (nix run github:marola-dev/marola-devkit/<tag>#<tool>) and has an
offline --self-test. Most take --dry-run, which prints the mutating git/gh calls instead of
making them. A tool that calls gh needs a login on the host; inside ai-jail it has none, so use
--dry-run there or pass GH_TOKEN.
Pull requests and stacks
Tool
Script
Recipe
Does
pr-flow
pr.sh
just pr
The whole PR path from a finished commit: refuses on main or a dirty tree, fills a missing Tested: trailer (trailer-fill), pushes (stack pr for a mip-NNNN/k-* branch), writes the body (uprd). --dry-run prints every step and the body. Not named pr, which would shadow coreutils'
stack
stack.sh
just stack, just stack-link
Stacked branches for a MIP's tasks: start MIP-NNNN k slug (branch off the nearest lower task branch, else origin/main), pr (push, open or update with the right base), restack (after the base was squash-merged; --onto-base <sha> when it finds no fork point), status, branches, link (a GitHub Stack from the open PRs)
uprd
uprd.sh
just uprd
Write the branch's PR body from its commits, in the PR template's shape, and create the PR if none exists. uprd 84 works on that PR from any checkout; a file argument is used as the body. BASE, BRANCH and EXTRA_FILE override the base, the branch and an appended section
uprds
uprds.sh
just uprds
uprd for every PR of a MIP stack, bottom first, each with a shared "Stack" section (states, the current PR marked)
mip-stack
mip-stack.sh
just mip-stack
Chain every open MIP draft PR into one GitHub Stack, resolving the shared docs/MIPs/README.md row conflict; --resume, --skip <PR#>, status, clean
docs-mip-stack
docs-mip-stack.sh
just docs-mip-stack
Chain un-merged docs/mip-NNNN-* branches: auto (push and plan the unambiguous local ones), list, plan <branch>… (conflict check with git merge-tree, prints the gh pr create lines)
deps-stack
deps-stack.sh
just deps-stack
Chain open dependency-update PRs into one GitHub Stack, auto-resolving bump-vs-bump conflicts; --resume, --skip, --include-steward, status, clean. Built in .tmp/wt-deps-stack, never the caller's checkout
deps-merge
deps-merge.sh
just deps-merge
Merge every open dependency PR that is mergeable with every check green (skipped and neutral count as green); reports the rest. --repo OWNER/REPO --unattended, for a scheduled job, also needs Dependabot as the author of the PR and every commit, no hold label, only Python manifests changed (requirements*.txt, pyproject.toml, a lock file), and each changed line one dependency whose versions moved by a patch or minor (a 0.x minor counts as major), read from the diff; it pins the merge to the checked head commit and writes one stderr line per refused PR. The caller checks AUTO_MERGE
bump-merge
bump-merge.sh
just bump-merge
Squash-merge the newest release's chore/devkit-vX.Y.Z PRs in every consumers.txt repo the umbrella first, with an optional soak and canary delay (both off by default); --dry-run prints each decision and command, --now EPOCH sets the clock. Run hourly and on bump-ready by bump-merge.yml; the rules are in workflows
branches
branches.sh
just branches-clean, just branches-open
clean deletes local branches whose PR merged (local and remote ref); open opens a main-based PR for each plain branch ahead of main with none
just stack-setup, just stack-view and just stack-sync wrap GitHub's own gh stack
extension rather than a devkit tool.
Trailers
Tool
Script
Recipe
Does
trailer-fill
trailer-fill.sh
just trailer-fill
Add a missing Tested: (ci-only) trailer to each commit of the branch, dates preserved, and a Closes #N line for a task branch whose row links an issue (TASK_PARTIAL=1 opts out). --base <ref> bounds the rewrite; --dry-run changes nothing
Issues and the board
issues is the whole issue surface (MIP-0063, MIP-0070 §5.7). Every subcommand takes
--dry-run; the reads behind it still happen, so a login is needed either way.
Recipe
Command
Does
just issue-queue [--milestone NAME]
issues queue
The unassigned open issues across the umbrella's org, agent-ready ones from other repos as owner/repo#N, sorted size then priority, with ready, blocked and in-triage counts from labels and dependency edges
just issue-ready <n>
issues ready
The five-rule Definition of Ready, naming the rule that failed; adds agent-ready on a pass and removes it on a regression
just issue-claim <n|owner/repo#n>
issues claim
Re-check readiness, assign to you, drop the label, set the board to In progress, print the stack start line
just tasks-to-issues MIP-NNNN [--milestone NAME] [--deliverable NAME]
issues tasks-to-issues
File a MIP's task table: a parent in the umbrella, one sub-issue per row in the repo its delivers cell names, blocked by edges per depends on, everything on Project 1. Idempotent; sets no area/*, layer/* or size/*
just milestone-new "<name>" [--mip MIP-NNNN]
issues milestone new
Create a deliverable milestone; idempotent
just labels-sync [--prune] [--force]
issues labels sync
Reconcile the repo's labels with the devkit's .github/labels.yml; orphans are deleted only with --prune
just board-sync
issues board sync
Put every open issue on the board; set Status only on a card with none or Backlog, and move a closed issue's card to Done
(none)
issues board setup, issues board gates
One-time bootstraps: the board's Status options, views and Deliverable field (needs project scope); the five phase gate issues
(none)
issues sub add <parent> <child>, issues deps add <n> --blocked-by <m>, issues deps list <n>
One sub-issue or dependency edge by hand
Labels, rulesets and repo checks
Tool
Script
Recipe
Does
pr-label
pr-label.sh
just pr-label
Apply the deterministic area/*, layer/*, kind/* taxonomy (scripts/lib/pr_labels.sh) to one PR
backfill-pr-labels
backfill-pr-labels.sh
just pr-labels-backfill
The same for every merged or closed PR with no label; --limit, --nlp (compare with pr-label-nlp), --nlp-apply-unscoped (fill only an area/unscoped gap)
pr-label-nlp
pr_label_nlp.py
(none)
A local TF-IDF classifier for area/*, kept to compare against the deterministic one, never applied on its own. --pr N or --title/--body
ruleset-sync
ruleset-sync.sh
just rulesets-check, just rulesets-apply
check [owner/repo…] diffs a repo's live branch rulesets, matched by name, against each manifest in .github/rulesets/: main-rule.json (a squash PR with 1 approval; org and repo admins bypass on a PR), plus that repo's bypass actors in .github/rulesets/bypass-extras.json (each with a reason; the @consumers key applies to every repo in .github/consumers.txt, --consumers FILE to override), and ci-rule.json (ci-ok from GitHub Actions, integration 15368, non-strict; no bypass actors, and no extras); prints <repo> <ruleset>: ok|drifted|missing; apply <owner/repo…> creates or updates each; --manifest FILE, once or more, replaces the set; --all-org ORG covers an org
merge-key-check
merge_key_check.py
just quality
Fails when MERGE_BOT_KEY appears in a workflow outside the allowlist .github/merge-key-allowlist (workflow files, one repo-relative path per line, # comments), or in a job that runs third-party actions or fetch/build commands, calls a reusable workflow (except a listed local one), sets container/services or a shell other than bash or sh, or sits beside workflow-level env carrying the key (MIP-0086 §5.6). Once the allowlist exists, secrets: inherit is flagged repo-wide
agents-check
agents-check.sh
just agents-check
Compare AGENTS.md's invariants block byte for byte with the pinned devkit's agents/invariants.md (--block or MAROLA_INVARIANTS_BLOCK to use another)
docs-lint
docs_lint.py
(none)
MIP-0074 §7's stale-content check over README.md and docs/**/*.md: undefined or unmarked foreign recipes, another repo's paths, split-era wording, docs/index.md, links leaving the repo; and a MIP §11 bullet that says decided, not negated or pending ("not decided", "once that's decided"), without a **Decided YYYY-MM-DD:** marker (MIP-0085 §5.6)
wiring
wiring.py
just wiring
MIP-0076 §5.1's four tables (artifact, dispatch, pin bump, deploy) and MIP-0085 §5.3's contracts (a data file, .json, .yaml, .csv, .sql and the like, copied byte for byte into another repo, outside dot-directories and lock files; the first by path is canonical, a test copy never), parsed from the umbrella's and every submodule's workflows, pin files, scripts, justfiles, Dockerfiles, compose files and build.sbt, with each devkit workflow call's ref (each call is read at that ref: from a local devkit clone's tags, else fetched from GitHub, a tag cached under ~/.cache/marola-wiring). wiring [FILE] prints the block or rewrites it between FILE's wiring:start/wiring:end markers; --check [FILE] fails on a stale block, an orphan or unreadable dispatch, or an artifact nobody reads that the umbrella's wiring.allow (<artifact>: <reason> per line) does not list; a contract <canonical> <copy>…: <reason> line there names the canonical file, or links copies that differ on purpose, and fails the check when a path in it is gone; run it in an umbrella checkout with submodules. --lint-devkit [DIR] fails on the devkit's own workflows: a dispatch it cannot read, or a type they listen for and none sends (devkit CI runs it)
decisions
decisions.py
just decisions
MIP-0085 §5.6: one row per decision in the MIPs (MIP-NNNN-*.md under --mips, docs/MIPs by default): §1's first sentence, each top-level "Alternatives considered" bullet's bold lead and first clause, shown as <lead> (rejected): <clause> (a bullet with several bold-led sentences gives a row each), and every **Decided YYYY-MM-DD:** … sentence outside code, each with the MIP's section and the first words of its Status row. decisions writes DECISIONS.md in that directory, or --md FILE; --json FILE writes the rows as {"rows": [{mip, file, section, status, kind, text}]}, the bundle's decisions.json. --check [FILE] fails when the file differs from what the MIPs give
doc-code
doc_code.py
just doc-code
MIP-0085 §5.4: which docs name which code. --root <umbrella> --graph <graph.json> scans each repo's docs/**/*.md and every README (the umbrella's and each submodule's, outside fenced code) for backticked tokens; one links when it is a tracked file's path, path suffix or basename (Markdown files excepted), or else a graph node's name (the last segment, at least 4 characters; a bare lowercase word only as name() or Type.name). A name in several files links only when its paragraph names the repo or a path segment of exactly one of them; otherwise it is kept as ambiguous with every candidate. --md FILE writes DOC-CODE.md (file level, by MIP and page) and refuses to replace it with fewer than half its rows (§8: delete it to accept); --json FILE writes doc_code.json for the routing bundle (symbol level, docs by doc and code by file); --check FILE fails when FILE is stale
workflow-runners
workflow_runners.py
(none)
Fail when any workflow can reach a self-hosted runner (runners)
skills-vendor
skills_vendor.py
just skills-check, just skills-outdated, just skills-update
Pin vendored skills to an upstream commit in skills.lock (below)
skills-vendor
A skill copied from another repo is pinned the way a dependency is (MIP-0080): skills.lock
beside the skill directories names its upstream, the commit it was taken at and the git blob sha
of every vendored file. The lock is <repo>/.claude/skills/skills.lock unless --lock says
otherwise, and a skill's files live in <lock dir>/<name>/;
this repo's
shows the shape.
Field
Meaning
upstream, path
The GitHub repo and the skill's directory in it ("" for the root)
commit
The upstream commit the files were taken at; null when init found none
files
Each vendored file with its git blob sha. Only these files are vendored
paths
Optional: a vendored file that sits elsewhere upstream ("LICENSE": "LICENSE")
license
The SPDX id from SKILL.md's license: or the LICENSE file, or null
local_edits
Optional: a patch relative to the lock directory, re-applied after every update; files then holds the edited shas and upstream_files the pristine ones
hold
A reason to leave the skill where it is; outdated and update skip it
Command
Does
check
Offline: every file in each skill directory has the sha in files. No lock file: exit 0
outdated [--min-age DAYS] [--json] [--strict]
The newest upstream commit touching path at least --min-age days old, with a diff of what it changes
Fetch at that commit, re-apply local_edits (a patch that fails stops it), rewrite the lock; never commits
init <name> --upstream o/r --path DIR [--sha SHA] [--paths FILE=UPSTREAM]
A lock entry from the copy already in the tree; without --sha, the newest of 200 upstream commits whose tree matches
Upstreams are read through a blobless git clone of https://github.com/<upstream>, so the tool
works wherever that does, Actions included.
| mip-resolve | mip-resolve.sh | (none) | Print a MIP's doc or .tasks.md (MIP-NNNN [tasks]), or with --path where it was found, by the lookup in design |
| api-docs-push | api-docs-push.sh | (none) | api-docs-push <dir> <remote> <sha> [--branch api-docs]: force-push a directory as one orphan commit, the api-docs workflow's push step |
Code graph
Tool
Script
Recipe
Does
graph
graph.sh
just graph
MIP-0076 §5.2's pinned graphify. build runs extract . --code-only --no-label, prunes every node without a source file (library and package nodes such as String), the edges touching them and self-loops, then cluster-only . --no-label --graph <graph.json> twice: the first assigns communities, and the second, given .graphify_labels.json naming each after its highest-degree member (a file node only when nothing else is there), writes the report and HTML with those names. Last, it tags entry points entry: true (a node whose line is def main(, @main or object X extends …App, and the file node of a file with a __main__ guard), which route query boosts (MIP-0085 §5.2). All of it lands in ${XDG_CACHE_HOME:-~/.cache}/marola-graph/<repo>, with no update (no Markdown nodes), recording HEAD and git submodule status in build.json; an uninitialised submodule fails it. query (--budget 400 by default), path and explain read that graph and warn when it is stale. Every call runs under env -i with five variables, and unshare -rn where allowed; --backend is refused, and so are extract and update, which would overwrite the pruned graph. Bumping graphify: Updating graphify
route
skills/routing/scripts/route.py
just route
MIP-0084 §5.2: the code graph and the wiring with no Nix and no graphify, Python stdlib only. query "<identifiers>" [--budget 400], explain <node>, path <a> <b> and wiring <term> read a local graph build whose build.json matches HEAD and the submodules, else refs/marola/routing-bundle, else origin/routing-bundle, and say which on their first line. query splits identifiers on case and separators (OpenMeteoClient is open, meteo, client), drops stop words such as where, the, entry and point, and boosts a node tagged as an entry point when the question says entry, start or main (MIP-0085 §5.2). `impact <node
route-usage
route_usage.py
just route-usage
MIP-0084 §5.5: one line per Claude Code session under ${CLAUDE_CONFIG_DIR:-~/.claude}/projects (or the directories given) that ran route, graph query/explain/path/build, or read the umbrella's wiring block, with the counts, prompted when a person's message before the first call named route, routing, graph or wiring, and hook when the session-start ▸ routing: line was in its context; then a total. A subagent's transcript counts toward its parent. Local and read-only; it prints counts, never transcript text
Self-hosted runner
Tool
Script
Recipe
Does
runner-preflight
runner-preflight.sh
just runner-preflight
Check this machine can run the workflows (nix, python3, node, jq, curl, docker, no passwordless sudo, disk)
gha-runner
gha-runner.sh
just runner-up, runner-down, runner-status, runner-logs
Run a registered runner in the background from GHA_RUNNER_DIR
setup-runners
setup-runners.sh
just runners
Register N runners on this machine as user services; --status, --remove
temps
temps.sh
just temps
CPU and GPU temperature against the parts' own throttle and shutdown points; --watch, --json
Details and variables are on the runners and
config pages.