Skip to content

kew epic — Epic dependency-graph build, validate, and drive

kew epic turns a set of work items into a dependency graph, then — from Phase 2a on — drives that graph: dispatching ready nodes in dependency order and mechanically merging PRs a human has already approved.

Where the epic lives depends on the configured tracker (see [tracker] configuration):

Tracker Epic membership Dependency edges Driver support
github (default) issues carrying the epic:<name> label <!-- kew:depends N M --> markers in issue bodies advance, run, and retry
linear admitted issues in the Linear project named <name> Linear's native blocks / blocked-by relations advance, run, and retry

See also the launch runbook for release and publication procedures.

On Linear, native blocks/blocked-by relations are the DAG, but a project is only the candidate population. An epic item is dispatchable only when it is in an unstarted/Todo state and carries both the kew membership label and the kew:ready human-arm label. Backlog and Triage items, unarmed Todo items, and items with missing admission facts remain queued; they do not enter the frontier. Clearing kew:ready withdraws admission on the next tick.

For example, in a Linear project:

Admission state Linear item Labels Driver behavior
Queued SFD-101 (unstarted/Todo) kew only Remains queued; it is not dispatched
Armed SFD-102 (unstarted/Todo) kew + kew:ready Enters the frontier when its dependencies are satisfied

The delegation rail is separate: an explicitly delegated, non-epic Linear item (normally assigned to the configured kew member) may be claimed by kew delegate watch. Project membership alone never implies delegation, and an item admitted to the epic rail is excluded from delegation. The two rails share the item-id reservation and fleet cap, so one item cannot be claimed by both.

Auth is environment-only (LINEAR_API_KEY); the key is never put in kew.toml, issue content, or logs. With tracker.type = "linear", kew reads membership, admission, and native dependency state from Linear, reads PR, review, and CI facts from GitHub, and closes the Linear issue after its GitHub PR is observed merged. OAuth/app identity work from #837 is not required for this single, firm-operated member/API-key contract.

The Linear project query includes every issue in the project, including archived issues, so an archived completed dependency remains available to satisfy a native edge. A Linear issue with children is an organizer/container, not a work node: kew keeps its bounded hierarchy metadata for diagnostics but never dispatches it and never turns parent/child hierarchy into a dependency edge. Native relations between actual work items remain intact.

All subcommands take the epic name as their first argument: the part after epic: on GitHub (an epic labeled epic:auth-rework is kew epic <cmd> auth-rework), or the project name on Linear.

kew epic plan — turn a plan into a drivable epic

Usage: kew epic plan <name> [OPTIONS]

Options:
  --from PATH           Implementation plan file to turn into issues
  --from-manifest PATH  Create from an existing manifest (skips the planning agent)
  --dry-run             Validate and preview; create nothing
  --amend               Append new nodes to an existing epic instead of refusing it
kew epic plan checkout-flow --from docs/plans/2026-07-02-checkout.md
kew epic plan checkout-flow --from docs/plans/2026-07-02-checkout.md --dry-run
kew epic plan checkout-flow --from-manifest .kew-logs/epic-plan-checkout-flow-20260702-101500.json
kew epic plan checkout-flow --from docs/plans/2026-07-02-checkout-v2.md --amend

A governed dispatch of the planning agent — the agent provides the judgment, kew provides the guardrails. The agent reads the plan, browses the codebase, and batches tasks into well-sized draft issues, written to a JSON manifest — not to GitHub. Then kew takes over:

  • the draft DAG is validated (cycles, dangling deps, plan reference in every body) before anything is created
  • kew creates the issues itself, in dependency order, applying epic:<name> plus the priority/complexity/type labels — ready is never applied (the manifest has no free-form label field)
  • <!-- kew:depends --> markers are written by kew from the manifest's depends_on, using real issue numbers
  • a mandatory validate gate re-checks the real graph after creation, then prints the Mermaid render
  • the epic is registered in the local run store the moment its issues are created, so kew epic run --all sees it before its first tick (a freshly-planned epic used to be invisible to --all until driven by hand)

Every run saves its manifest under logging.dir; --from-manifest re-uses one without re-running the agent (cheap retry after --dry-run or a mid-creation failure). Re-planning an epic that already has issues (open or closed) is refused by default — bare epic plan plans a fresh epic only. Requires the GitHub tracker; unlike the driver commands, epic plan does not create Linear projects or issues.

Amending a running epic. --amend appends new nodes to an existing epic through the same governed pipeline. New local_ids must not collide with the epic's existing node ids, the <=10 target_files dispatchability gate still applies, and depends_on may reference either other new nodes or the numbers of existing epic issues — kew writes <!-- kew:depends --> markers for both, then re-validates the merged graph after creation.

Commit the plan before driving. Every issue body points dispatch agents at the plan file, and dispatch worktrees are cut from origin/<base> — if the plan isn't pushed there, the agents can't read it (epic plan warns when it detects this). Review the created issues, commit/push the plan, then start the driver: kew epic run <name>.

kew epic graph and kew epic validate

Usage: kew epic graph <name> [OPTIONS]
Usage: kew epic validate <name> [OPTIONS]

Options:
  --format TEXT      mermaid or dot (graph only; default: mermaid)
  --epic-prefix TEXT  Label prefix identifying epic membership (default: "epic:")
  • kew epic graph <name> renders the dependency DAG as a Mermaid diagram (default) or Graphviz DOT, color-coded by each node's provider-derived status (done/ready/in_progress/in_review/blocked/failed/todo).
  • kew epic validate <name> checks the graph for self-edges, dangling dependencies (an edge to an issue outside the epic that doesn't exist), cycles, and orphans, and prints a topological level count on success. Exits non-zero if the graph is not a valid DAG.

Both commands are read-only — they never dispatch, merge, or write to the run store.

kew epic status

Usage: kew epic status <name> [OPTIONS]

Options:
  --epic-prefix TEXT  Label prefix identifying epic membership (default: "epic:")

Read-only snapshot of the driver's view of the epic: per-status node counts, the dispatch frontier (nodes ready to dispatch next), in-flight nodes, held nodes (approved but waiting on CI), failed/cancelled nodes, stuck nodes with their blast radius, a done/remaining count, and the tri-state signal (complete/progressing/stalled — see below). This is the same overlay/frontier logic kew epic advance uses, but status never mutates anything — no dispatch, no merge, no run-store writes.

When this epic's per-epic driver lock is currently held, status also prints a Poller: epic=<name> pid=<pid> held-since=<ts> holds epic <name>'s driver lock line so you can see who is driving it right now, from which process, and since when (and why a concurrent run is locked out). If the recorded holder's process is no longer alive the line reads has a STALE hold on …. A stale lock — a dead-PID holder, or one whose heartbeat has not been refreshed within HEARTBEAT_STALE_SECONDS (300s) — is reclaimed automatically by the next driver that tries to acquire it (the steal is logged), so a TaskStop'd driver's orphaned lock never demotes later drivers to silent observers. --takeover (see below) is only needed for a legacy lock that predates heartbeats and whose staleness can't be proven. Because locks are keyed per epic, a different epic being driven never shows up here.

kew epic status auth-rework

kew epic advance — run one driver tick

Usage: kew epic advance <name> [OPTIONS]

Options:
  --max-parallel INTEGER   Override config.epic.max_parallel for this tick.
  --takeover               Also reclaim a legacy no-heartbeat lock (a provably-stale lock is reclaimed automatically).
  --epic-prefix TEXT       Label prefix identifying epic membership (default: "epic:")

Runs a single tick of the epic driver:

  1. Acquires this epic's per-epic driver lock (see below); a concurrent advance/run on the SAME epic that can't acquire it exits cleanly, naming the live holder (epic=<name> pid=<pid> held-since=<ts>) and its heartbeat age, plus the manual workaround: its agents keep running; tick it manually: kew epic advance <name>. A different epic driving concurrently never contends for this lock. A stale holder — a dead PID, or a heartbeat not refreshed within HEARTBEAT_STALE_SECONDS (300s, a frozen/hung driver) — is reclaimed automatically: the acquiring driver steals the lock and logs the steal, so no live epic is left permanently observing a dead driver's lock. A live holder with a fresh heartbeat is never taken over. --takeover is only for a legacy lock written before heartbeats existed, whose staleness cannot be proven (e.g. a recycled PID); it still never stomps a live, freshly-ticking driver.
  2. Re-derives the graph and computes each node's status overlay (provider facts + run-store history + epic-level hold/reservation state).
  3. Merge pass: for every node with an open PR, merges it if — and only if — the merge gate below is satisfied; otherwise leaves it open, held, or marks it stuck.
  4. Dispatch pass: computes the ready frontier (deps satisfied, not reserved/in-flight/FAILED/CANCELLED/BLOCKED/IN_REVIEW/HELD or awaiting_publication) and dispatches up to max_parallel - in_flight of them, through the normal budget/cost-approval/fleet-stop gates.
  5. Prints a tick summary and releases the lock.

If the epic's tracker query resolves to zero nodes — a typo'd epic name, or running from a cwd wired to the wrong tracker — advance errors (exit 2) naming the empty query (the GitHub label or the Linear project) instead of reporting COMPLETE done=0 remaining=0. A driver never declares success for work that does not exist; a legitimately-finished epic still reports complete via its non-empty DONE set.

If Linear is the tracker and its API is unreachable — a 502/503/504 gateway page or a reset connection — the client retries with bounded exponential backoff (~3 attempts). If the outage outlasts the retry budget, advance errors (exit 2) with a classified linear-unavailable diagnostic (a transient infrastructure blip to re-run, not a stuck epic) instead of dumping a traceback. Re-run once Linear recovers. A 4xx or a Linear GraphQL error still fails fast — retrying a client-side error only wastes time. No admission or write-back is inferred from a partial or unavailable response.

A completed host-publication run whose GitHub publication fails is surfaced as awaiting_publication. Its committed worktree is preserved and the node is not redispatched. Restore GitHub access or open a PR from the preserved branch; Linear-backed drivers reconcile an existing branch PR on their next tick. See also Troubleshooting deferred publication.

kew epic advance auth-rework
kew epic advance auth-rework --max-parallel 2

Sample output:

epic auth-rework: tick signal=progressing
  dispatched: ['42', '43']
  merged: ['40']
  held: ['41']
  in_flight: ['42', '43']
done=3 remaining=5

kew epic run — poll wrapper

Usage: kew epic run [name] [OPTIONS]

Options:
  --interval INTEGER       Seconds between ticks while PROGRESSING (default: 60).
  --max-parallel INTEGER   Override config.epic.max_parallel for every tick.
  --all                    Drive every known epic round-robin in one poll loop.
  --takeover               Also reclaim a legacy no-heartbeat lock (a provably-stale lock is reclaimed automatically).
  --epic-prefix TEXT       Label prefix identifying epic membership (default: "epic:")

Repeatedly calls advance every --interval seconds until the epic completes or needs repair:

  • complete — every node is DONE. Exits 0.
  • awaiting_merges — the frontier is empty and nothing is in-flight, but the only remaining work is PRs open in review awaiting a human's merge (and nothing is stuck). A non-alarming line lists the pending PRs and the driver keeps polling. A hand-merge is observed on a later tick, which clears configured review labels and advances the frontier.
  • stalled — the frontier is empty, nothing is in-flight, no hold is live, and work remains that needs an operator. Exits non-zero, with the stuck-node blast radius printed, so it never loops forever on a dead DAG.

An approved PR that's still waiting on CI reads as progressing (HELD), not stalled — kew epic run keeps polling instead of giving up on work that's legitimately in flight.

Exit codes

kew epic run's exit code is API — orchestrating wrappers, cron jobs, and background-task harnesses branch on it, so the codes are stable and documented:

Code Meaning What to do
0 complete — every node is DONE. Nothing remains.
2 linear-unavailable — Linear's API stayed unreachable past the client's bounded retry. A transient blip, not operator repair. Re-run once Linear recovers.
1 stalled with a failed-terminal, cancelled, held, or otherwise operator-repair node. Repair the node(s) — e.g. kew epic retry <name> <node>, or kew epic unhold <name> <item> to release a wedged hold — then re-run.

awaiting_merges is nonterminal for both single and --all drivers. 1 stays the conservative "needs repair" code, and 2 is reserved for classified infrastructure or configuration failures.

kew epic run auth-rework
kew epic run auth-rework --interval 30 --max-parallel 3

Run it under a scheduler (cron, a systemd timer, a loop of your own) to drive an epic unattended — it self-terminates instead of running forever.

Like kew epic advance, if Linear is the tracker and its API is unreachable past the client's bounded retry, kew epic run errors (exit 2) with the same classified linear-unavailable diagnostic — a transient blip to re-run, never a traceback — and never enters the poll loop.

--all — drive every epic from one loop

kew epic run --all advances every known epic round-robin in a single poll loop: each tick advances each not-yet-terminal epic once, acquiring and releasing that epic's own per-epic lock — so two epics never lock each other out and can tick concurrently. A "known" epic is any epic recorded in the local run store — that includes freshly-planned epics (kew epic plan registers them at creation time) as well as epics with driver history. At startup --all logs the discovered roster and its discovery source (the local dispatch.db), so a missing epic is diagnosable from the output. Per-epic signals are reported per line, and the loop exits only when all epics are terminal. The aggregate exit code is the worst of the per-epic codes: 1 (any epic needs repair) beats 2 (any epic is merely awaiting merge) beats 0 (every epic complete) — see Exit codes. This replaces stacking one kew epic run <name> per epic. If an epic's lock is already held by another live driver, --all surfaces the holder and skips that epic (it's being driven elsewhere) while continuing to round-robin the rest. An epic whose query resolves empty is surfaced with its diagnostic and counted as needing an operator (contributing exit 1 to the aggregate) — it is never silently dropped or read as complete. A Linear outage that outlasts the retry budget hits every epic on the roster: each is surfaced with the classified linear-unavailable diagnostic and marked as needing an operator (exit 1), and the round-robin keeps processing the rest rather than aborting on a traceback.

--all scopes itself to the active project's tracker. An epic whose recorded tracker differs from the configured [tracker] type — inferred from the shape of its recorded work-item ids (a numeric GitHub issue number vs. an opaque Linear key like SFD-12) — belongs to a different project and cannot be driven by the active tracker's sources. --all skips it with a one-line (skipped: tracker=linear) note. A skipped epic contributes to neither the STALLED classification nor the aggregate exit code — so a store that also holds seven complete Linear epics no longer makes a healthy GitHub run exit non-zero and demand an operator. An epic with no recorded items yet (freshly planned, never driven) has no inferable tracker and is driven under the active tracker rather than guessed away. If no discovered epic belongs to the active tracker, --all reports that nothing is in scope and exits 0.

When every epic is terminal, --all prints one combined fleet summary that names every driven epic under its exit classification — complete, awaiting-merges, stalled, or locked-out (driven elsewhere). The summary is printed regardless of the aggregate exit code, so a mixed-outcome fleet stays legible: the complete and awaiting-merges epics are still named even when the worst-wins exit only reflects the stalled ones needing an operator.

kew epic run --all
kew epic run --all --interval 30

kew epic resume — continue preserved work

Usage: kew epic resume <name> <node>

epic resume is the explicit recovery path for a dirty worktree preserved after its dispatch owner died. Before claiming anything, kew verifies that the recorded checkout is inside [branches] worktree_base, is still on the recorded branch, is still dirty, and has no live or unproven owner. A missing, clean, escaped, wrong-branch, or concurrently owned checkout is refused without changing the continuation record.

The successful claim and its fleet worker slot are one transactional operation. The continuation then runs the same item against the existing checkout and uses the normal host-publication broker, so existing edits are not replaced by a fresh checkout. Two concurrent resume callers produce at most one continuation. A failed continuation returns to awaiting_continuation for a later operator attempt; a successful published continuation is terminal.

kew epic resume auth-rework 87
kew epic resume auth-rework SFD-12

kew epic retry — re-admit a failed node

Usage: kew epic retry <name> <node> [OPTIONS]

Options:
  --epic-prefix TEXT  Label prefix identifying epic membership (default: "epic:")

<node> is the GitHub issue number. Re-admits a FAILED node to the dispatch frontier without deleting its run history:

  • Restores the configured ready label and clears failure labels in one edit.
  • Releases only an observed terminal reservation generation; live or indeterminate reservations survive.
  • Records a non-destructive retry marker so the driver's status overlay treats the stale failed run as superseded (rather than discarding it) — the node falls through to its provider-derived status (typically back to TODO, re-entering the frontier on the next tick).

Cost and token history from the failed run(s) are preserved — kew report still shows them. A run that fails after the retry marker is a fresh failure, not the superseded stale one.

Retry never starts an unsafe background process. If the epic lock is not held, it exits nonzero with no live driver — relaunch required: kew epic run <name>; an active or indeterminate lock is treated as live.

kew epic retry auth-rework 87

kew epic unhold — release a driver hold

Usage: kew epic unhold <name> <item> [OPTIONS]

Options:
  --epic-prefix TEXT  Label prefix identifying epic membership (default: "epic:")

<item> is the work-item id (a GitHub issue number, or a tracker id like SFD-12). The operator counterpart to the driver's own post-terminal hold bookkeeping: when a sticky hold (typically ci_pending) wins overlay precedence and wedges the node — the case the containment arc kept clearing by hand via raw store calls — unhold releases it:

  • Clears the epic_holds row for that epic/item via the store API.
  • On a GitHub tracker, also strips the stale pr-pending-review label (mirror of the manual unwedge playbook). A non-github tracker owns node state in the tracker itself, so no label is touched.
  • Prints the cleared reason and the hold's held-since timestamp (UTC).

Exits 2 with a clear message when <item> is not a valid item id for the configured tracker, or when no hold exists for that epic/item.

This is where a hold fits the operator loop: a sticky hold makes kew epic run exit 1 (stalled, needs repair — see Exit codes). unhold is the manual escape hatch when no automatic path clears the row — see Hold semantics for the full set/clear map.

kew epic unhold auth-rework 87
kew epic unhold auth-rework SFD-12

The merge gate

The driver merges a node's PR only when both of these hold:

  1. A verified human approval. GitHub's own reviewDecision == APPROVED computation (not the mere presence of a review — this accounts for reviews GitHub has marked stale after a new push), from a reviewer who is not the PR author, is a GitHub User rather than a bot, and currently has write, maintain, or admin repository permission. Kew reads that permission fresh for every merge decision and fails closed if the lookup or identity evidence is missing. When [epic] approvers is non-empty, the reviewer must also be in that allowlist.
  2. CI-green. The PR's effective required status checks are green, verified via mergeStateStatus == CLEAN (read from GitHub's own effective-rules computation, which understands both classic branch protection and rulesets).

An approved PR that isn't green yet is HELD (ci_pending) — not stuck — and the driver merges it automatically the moment CI turns green, so you don't have to babysit the timing. A genuinely red or pending required check (or a stale base) is a sticky hold the operator needs to look at.

The merge itself is always:

gh pr merge <pr> --repo <repo> --squash --match-head-commit <sha>

— never --admin or --force (those flags let gh merge over a red or pending required check, which would silently defeat the CI-green gate above). No label ever triggers a merge — a dispatched agent's gh token could set a label on its own PR, so a label is not a trustworthy signal.

For a Linear epic, this is still a human merge gate on GitHub. Linear is the work-item and lifecycle system; GitHub remains authoritative for the PR, review, CI, and merge decision. An operator may merge the PR externally after the required human approval and green CI. The next driver tick reconciles that merged PR and writes Linear Done; Linear Completed by itself never proves that the PR merged.

The governed-review invariant

When the governed adversarial-review stage is enabled in blocking mode ([review].adversarial = true, [review].blocking = true), the merge pass gains one more invariant, checked at merge time for every policy-covered candidate:

merge only if the latest verdict is READY ∧ verdict.head_sha == the current head SHA ∧ the approval timestamp postdates the verdict.

This is enforced at the merge pass itself, not the status label — HELD is a live merge candidate, so an approved + green-CI HELD node with a non-READY verdict is skipped, loudly, with an attested reason. It closes three holes at once: a HELD-is-mergeable bypass, a post-verdict force-push merging unreviewed code (merged-SHA must equal reviewed-SHA), and a stale pre-existing approval auto-merging (the approval must postdate the verdict, else it surfaces as "re-approve"). Nodes already in review when the flag flipped are grandfathered (PRE_POLICY) and merge under the pre-existing approval + CI rules above.

The verdict is evidence for the human approval, never a substitute — there is still no auto-merge on READY. Each round's verdict is a chain-attested adversarial_review_verdict event (see kew audit / the audit trail) binding the PR, item, head SHA, policy in force, and review-pool spend, so "did the merged code survive review" is answerable from the chain alone.

There is no --auto-merge

Phase 2a deliberately does not offer an autonomous, no-human merge mode. An adversarial design pass found the concept unsound — there is no forgery-proof way to mark a PR "safe to merge without review" (any label a dispatched agent's own credential can set is exactly as forgeable as the signal it would replace), and gating on "required review satisfied" is self-contradictory (a PR can't be CI-green under a review-required branch until it's approved, so autonomous merge would have to fake its own approval). It's also the case kew's own thesis argues against — see Philosophy: "Offload the work. Keep the judgment." The full reasoning is recorded in the design spec (docs/superpowers/specs/2026-07-01-epic-dag-phase-2-driver-design.md, "Why autonomous --auto-merge was cut").

Hold semantics — who sets and who clears a hold

A hold is an epic_holds row keyed on the epic and the work-item id. While a hold is live it wins overlay precedence and routes the node to HELD (see The terminal signal) — the driver keeps polling that node instead of dispatching or merging it. Two subsystems set holds; several paths clear them:

Reason Set by Cleared by
ci_pending The merge pass (_classify_open, graph/driver/merge.py) when an approved PR isn't CI-green — a real non-green mergeStateStatus (BLOCKED/BEHIND/DIRTY/UNSTABLE). A transient UNKNOWN is not held; it re-polls next tick. _do_merge on a later tick, the moment CI turns green and the driver merges; the out-of-band MERGED / closed-unmerged bookkeeping (_handle_merged / _handle_closed_unmerged, #816/#818) when a terminal PR kew didn't merge itself still carries the hold; or kew epic unhold.
budget_preflight:* The dispatch budget gate (#661) when preflight refuses a node over its cost cap — the reason string carries the observed/cap numbers and the knob to raise. _reconcile_budget_holds at the start of each tick, once the cap admits the node again (raise budget.<limit>_usd, or kew dispatch <n> --force); or kew epic unhold.

kew epic unhold is the manual escape hatch for either row when a hold is stuck and no automatic path will clear it — see kew epic unhold.

The terminal signal

Every tick (advance, and the read-only status) reports one of these signals:

Signal Meaning
complete Every node is DONE.
progressing Something moved this tick (a dispatch or a live in-flight run), or at least one node is HELD on ci_pending within its stall clock — an approved PR waiting on CI is legitimate progress, not a stall.
awaiting_merges The frontier is empty and nothing is in-flight, but the only remaining work is one or more PRs open in review awaiting a human's merge (and nothing is stuck) — the normal human-gate state, not a broken pipeline. A running driver keeps polling and observes the merge on a later tick. Only advance (which runs the merge pass) emits this; the read-only status reports the underlying stalled instead.
stalled The frontier is empty, nothing is in-flight, no hold is live, and not everything is DONE — remaining work is FAILED, CANCELLED, or stuck and needs an operator.

kew epic run uses this signal to decide whether to keep polling (complete → exit 0, stalled → exit non-zero, awaiting_merges/progressing → keep going). awaiting_merges prints a non-alarming line listing the pending PRs; a stalled tick exits 1 when a node needs operator repair — see Exit codes.

Fleet isolation

The fleet dispatcher never selects an epic-labeled issue, even if it also carries ready — epic membership is a positive exclusion in the fleet's issue selector, and it also excludes epic PRs from the auto-conflict-resolver so the fleet can't force-push a branch the driver is tracking. Epic nodes are driver-owned: only kew epic advance/run ever dispatch or merge them.

Config

See [epic] in Configuration for max_parallel, merge_strategy, stall_days, approvers, and lock_path.

Tick journal

Every kew epic advance and kew epic run tick appends a changed-only snapshot of the driver state to the epic_ticks table in dispatch.db — capturing the signal, per-node statuses, counts, edges, and frontier, plus the actions taken that tick (dispatched / merged / held / stuck) and a hash of the snapshot state. The driver also stamps the epic_driver_state heartbeat table twice per tick: once at the start of the tick (after tool preflight succeeds, inside the single-driver lock) and once at the end of the tick (still inside the single-driver lock), recording the tick's duration, its terminal signal, and a reference (last_tick_id) to the corresponding epic_ticks row.

kew epic status and the read-only commands (graph, validate) never journal — they only read the driver's current view without persisting history.

The journal feeds the web UI's /epics endpoint (Phase B/C) to display a time-lapse replay of the epic's progress across ticks. Journal rows are never pruned in v1 — the full history remains for audit and replay. See the web view: ../web/epics.md.