SFEP-0001
SFEP Purpose and Process
- Status
- Accepted
- Type
- process
- Created
- Updated
- Author
- agent:Sailbot (drafted); project owner (direction + decisions)
SFEP-0001 — SFEP Purpose and Process
SFEP stands for Sailfin Enhancement Proposal. This document defines what an SFEP is, when to write one, how it is numbered and reviewed, and the bar every proposal must clear — regardless of whether a human or an agent drafted it.
It is deliberately lighter than Python’s PEP process or Rust’s RFC process.
Sailfin is a small, self-hosting project moving fast toward 1.0; the proposal
system exists to give design decisions a consistent shape, a stable citable
identifier, and an honest status — not to add ceremony. Proposals live in this
repo (docs/proposals/) rather than an external repository: the design is close
to the code it governs, and the same PR can change both.
1. What an SFEP is (and is not)
An SFEP captures a forward-looking design decision for the language, runtime, or toolchain — something with enough surface area or longevity that the why deserves to be written down and cited from code and commits.
Write an SFEP for:
- A new or changed language feature (syntax, semantics, the effect system).
- A runtime / ABI design (memory model, ownership floor, concurrency, backend, syscall surface).
- A toolchain design (build pipeline,
sfn check/fmt/test, package management, CLI architecture). - A cross-cutting process or informational decision that future contributors will need the rationale for.
An SFEP is not the right home for several adjacent genres, which keep their existing locations:
| If the document is… | It belongs in… |
|---|---|
| A rule we already follow (import style, test envelope) | docs/conventions/ |
| A post-incident root-cause analysis | docs/rca/ |
| An operational playbook | docs/runbooks/ |
| A living migration/architecture tracker | root docs/*.md or docs/proposals/ (e.g. docs/proposals/0025-native-runtime-architecture.md) |
| A single-issue implementation design gate | docs/proposals/design-notes/ (see §9) |
| User-facing reference for a shipped feature | site/.../reference/spec/ |
| User-facing preview of a planned feature | site/.../reference/preview/ |
| Strategic market positioning | docs/strategy/ |
Decision tree: design decision → SFEP · rule we follow → convention · incident → rca · living tracker → root audit · one-issue design gate → design-note.
2. Identifier and filename
- Every SFEP has a number:
SFEP-NNNN, zero-padded to four digits (SFEP-0017). This is the citable identifier — use it in commits, issues, spec chapters, and code comments. - The file is named
NNNN-kebab-slug.mdand lives indocs/proposals/. A multi-file proposal may instead be a directoryNNNN-kebab-slug/whose00-overview.mdis the canonical entry point. SFEP-0001is reserved for this process document.- The registry is
docs/proposals/README.md— a table of every SFEP with its status and type. It is the source of truth for which numbers are taken.
Allocating a number
The next number is max(existing) + 1, read from the registry table. To avoid
two in-flight drafts claiming the same number:
- While a proposal is a Draft in review, it may carry
sfep: TBDin its front-matter and a temporarydraft-<slug>.mdfilename. - The number is fixed when the proposal merges: assign the next free number,
rename the file to
NNNN-<slug>.md, and add its row to the registry in the same PR.
For low-volume periods, allocating the number up front (and updating the registry in the PR) is fine — the registry merge is the arbiter if two PRs race.
3. Front-matter schema
Every SFEP begins with a YAML front-matter block so the registry and tooling can read status without parsing prose:
---sfep: 17 # integer, or TBD while draftingtitle: Hierarchical Effectsstatus: Accepted # see §4type: language # see §5created: 2026-06-07 # YYYY-MM-DD, first draftedupdated: 2026-06-26 # YYYY-MM-DD, last substantive changeauthor: "agent:compiler-architect; human review" # see §7tracking: SFN-774, SFN-775 # what implements this — see §3.1 (optional)supersedes: # SFEP-N this replaces (optional)superseded-by: # SFEP-N that replaced this (optional)graduates-to: reference/preview/hierarchical-effects.md # spec/preview home (optional)---The prose body follows. The front-matter is the authoritative, machine-readable
status; new SFEPs must not also carry a free-form **Status:** … **Date:** …
header block. Proposals migrated into this system may still contain a legacy prose
header beneath the front-matter — when one is next edited, trim the redundant
prose so the front-matter is the single source of truth.
3.1 tracking: — where the work is
tracking: answers exactly one question: where is the work that implements
this? It takes one of two forms, and which is correct depends on how much work
there is.
- Bounded SFEP — a handful of leaves, unlikely to grow: list the Linear issue
identifiers.
tracking: SFN-774, SFN-775, SFN-776, SFN-777. - Epic-scale SFEP whose Project exists to implement it and nothing else —
point at the Project URL, and enumerate under
design-gates:only the design-note issues whose verdicts bind implementation. SFEP-0048 is the worked example.
The dividing line is not a count, it is whether the list can stay true. A hand-copied list of identifiers is a mirror of a Linear rollup, and a mirror nobody re-syncs on every merge is worse than no list at all: a reader trusts it and gets a stale answer. SFEP-0048’s list sat at 21 entries while its Project held roughly 65. Where the URL form applies, noticing a list has drifted is the signal to convert it rather than restate it — restating only resets the clock on the same failure.
The URL form requires the Project to be 1:1 with the SFEP, and often it is not. A Project may be a long-running container serving several designs, in which case its URL over-claims: it would tell a reader that the whole Project implements this one proposal. A survey of six accepted SFEPs (2026-08-12) found the split runs both ways — SFEP-0061 and SFEP-0070 each own their Project exactly, while SFEP-0069’s eleven-issue zeroization arc sits inside a sixty-eight-issue Project carrying the entire crypto/TLS build-out, and SFEP-0068’s four-issue cross-target arc is one thread in a seventeen-issue Makefile-retirement Project.
For a slice like those, the identifier list is the correct form and is not drifting — it is scoped to the slice, and the gap between it and the Project total is the other designs’ work, not omissions. Measure before converting: a large gap is evidence of drift only once you have established that the Project implements this SFEP and nothing else.
Three rules on the value itself:
- Identifiers must be identifiable.
SFN-471or#1180, never a bare number —[690, 1683]names nothing a reader can look up. - GitHub
#Nis historical provenance only, and must be labelled as such:"#965, #839 (historical intake); SFN-17". Issues predating the 2026-07-07 Linear migration are legitimately recorded this way. A bare#Nis a GitHub PR or an external-contributor intake issue, never the thing a reader is waiting on (.claude/rules/code-style.md). - Empty is legitimate for a
Draftwith no filed work. It is not legitimate for anAcceptedSFEP whose work is underway — that SFEP has a Project, and the URL belongs here. Empty is a statement, not a placeholder to fill with a guess.
tracking: is a reader’s index, not a gate: nothing parses it, and no CI check
depends on it. That is precisely why it rots quietly, and why the URL form is
preferred wherever it applies.
4. Lifecycle
Draft ──▶ Accepted ──▶ Implemented │ │ ├──▶ Withdrawn (author/owner abandons) ├──▶ Rejected (considered and declined) └──▶ Superseded (replaced by a newer SFEP; set superseded-by)| Status | Meaning |
|---|---|
| Draft | Under authoring or review. Not yet approved for implementation. |
| Accepted | Design approved; cleared to implement. May be partially built. |
| Implemented | Shipped end-to-end and self-hosting (see the bar below). |
| Withdrawn | Abandoned before acceptance. |
| Rejected | Considered and declined. Kept for the record. |
| Superseded | Replaced by a newer SFEP. Set superseded-by. |
Where the status lives (see docs/conventions/issue-naming.md): the SFEP’s
status: front-matter is authoritative, mirrored by the implementing Linear
issue’s native status — not GitHub labels. The design-approved workflow-state
label is retired, and needs-design survives only as a public GitHub
external-intake hint (never applied to a Linear-native SFN-NNN issue). Loosely,
the design maps Draft → ungroomed, Accepted → design gate passed, and
Implemented → Stage1-readiness met.
The bar for Implemented is the Stage1 Readiness Checklist in CLAUDE.md:
parses → type/effect-checks → emits .sfn-asm → lowers to LLVM IR → has
regression coverage → self-hosts → passes sfn fmt --check → documented in
docs/status.md and the spec. “Parsed but not enforced” is not Implemented —
it stays Accepted. Do not mark a safety feature Implemented until it is
enforced end-to-end.
5. Type taxonomy
| Type | Scope |
|---|---|
| language | Syntax, semantics, type system, the effect system. |
| runtime | Runtime, ABI, memory model, ownership floor, concurrency, backend, syscalls. |
| tooling | Build pipeline, check/fmt/test, package management, CLI, diagnostics. |
| process | How the project itself operates (this document is one). |
| informational | Strategy input, design surveys, guidance with no single shipping artifact. |
language, runtime, and tooling are the standards track (they change what
ships). process and informational do not gate a release.
6. Required sections (the standard for every author)
A proposal — drafted by a human or an agent — must contain these sections. This
is the bar an outside contributor must clear; it is also the checklist an agent
fills. Incomplete proposals stay Draft.
- Summary — one paragraph: what changes and why it matters.
- Motivation — the problem, who hits it, why the status quo is insufficient.
- Design — the concrete proposal: syntax/semantics/APIs, worked examples.
- Effect & capability impact — how it interacts with the effect system and capability enforcement (Sailfin’s pillars). State “none” explicitly if so.
- Self-hosting impact — what compiler passes change, and how self-hosting is
preserved. (See
.claude/rules/selfhost-invariant.md.) - Alternatives considered — options weighed and why they lost. A proposal with no alternatives section is not ready.
- Stage1 readiness mapping — which checklist items (§4) are done vs. pending.
- Test plan — the regression coverage that will prove it (
compiler/tests/). - References — issues/epics, related SFEPs, spec/preview chapters, prior art.
Use template.md in this directory as the starting skeleton.
7. Authorship and provenance
Most Sailfin proposals to date have been agent-drafted, and that is expected to continue. The system records this honestly rather than hiding it:
- The
authorfield names the drafter. Use anagent:<role>prefix for agent-authored drafts (e.g.agent:compiler-architect), and add; human reviewor a name when a human meaningfully shaped or signed off on it. - Provenance is tracked, not gated. There is no required human “sponsor”
field. Acceptance authority lives where it already does: PR review plus the
SFEP/Linear status advance to
Accepted. A human reviewing and approving the PR is the human gate — we do not add a second one. - The required sections in §6 are the real quality bar. They apply identically to human and agent drafts. A proposal is judged on whether it clears that bar, not on who or what wrote it.
This keeps the door open for outside (human) contributors: the standard they must meet is written down (§6), uniform, and independent of authorship.
8. Workflow
Proposing:
- Copy
template.mdtodraft-<slug>.md(orNNNN-<slug>.mdif claiming a number now). Fill the front-matter (status: Draft) and the §6 sections. - Open a PR. Iterate in review.
- On acceptance, set
status: Accepted, assign/confirm the number, rename toNNNN-<slug>.md, and add the registry row — all in the merging PR.
Implementing: land the work behind the proposal; when it clears the Stage1
bar (§4), set status: Implemented, bump updated, and ensure graduates-to
points at the shipped spec chapter (and docs/status.md reflects it).
Graduating to user docs: a language/runtime SFEP’s user-facing surface
lives on the docs site — reference/preview/ while planned, moving to
reference/spec/ once Implemented. The SFEP remains the design record (the
why); the spec chapter is the normative reference (the what). Cross-link both.
Retiring: set status: Withdrawn/Rejected/Superseded and move the file to
archive/ (§9) if it is no longer load-bearing.
9. Directory layout
docs/proposals/ README.md # the registry/index + how-to (the source of truth for numbers) template.md # copy-to-start skeleton 0001-sfep-process.md # this document NNNN-<slug>.md # numbered SFEPs (Implemented ones stay here as the record) NNNN-<slug>/ # multi-file SFEPs; 00-overview.md is canonical design-notes/ # single-issue implementation design gates — NOT SFEPs archive/ # superseded / rejected / stale historical notesdesign-notes/holds design docs scoped to one issue/PR that informed an implementation but are not enhancement proposals (e.g. an ASAN-gated free discipline for one worker path). They keep descriptive slug filenames and need no SFEP number.archive/preserves history for proposals and work-notes that are no longer current (superseded designs, completed one-off migration audits, old phase notes). Nothing inarchive/is a live SFEP.
10. Relationship to the roadmap and issues
- The planning roadmap lives in Linear Initiatives and Projects; the public
roadmap is its reviewed projection under
docs/conventions/public-roadmap.md. It is strategic — epics, not proposals. An epic may spawn an SFEP for its design. - GitHub issues are session-sized work. An SFEP’s
trackingfield links the issue(s)/epic(s) implementing it; issues link back to the SFEP for the why. - An SFEP is the durable design record; issues and PRs are the execution.
When they disagree, the SFEP’s
statusis authoritative for design intent anddocs/status.mdis authoritative for what actually ships today.