SFEP-0014
Agent-Legible Build/Test Output
- Status
- Accepted
- Type
- tooling
- Created
- Updated
- Author
- agent:compiler-architect (original sketch); agent:Sailbot (2026-08 rewrite); human review
- Tracking
- SFN-725, SFN-726
SFEP-0014 — Agent-Legible Build/Test Output
Amendment (2026-08-05) — rehosted from
makeontosfn; the phase ledger recorded as regressed rather than delivered. This SFEP was one of the original architecture sketches folded into the SFEP system at its founding and was never revised against the tree it describes. Four things had gone wrong.First, it named the wrong host as durable. Its keystone is a bash wrapper around
make, and it justified that with “the Makefile is explicitly sanctioned orchestration” — a claim SFEP-0006 Stage D contradicts by deleting the Makefile (§3.4). Its envelope’s schema string itself namesmake, binding it to a host with a scheduled end date.Second, its headline deliverable has silently regressed to non-functional. The phase ledger — “
make checkreports which of its seven phases failed” — is listed among this document’s success metrics as achieved. It is not: all six phase detectors miss, and the solewarnpath is unreachable (§3.3). CI does not catch it because the guarding test replays the 2026-06 banners asechostubs.Third, its inventory is stale in specifics that matter to an implementer: the test envelope is
schema_version 2, not 1; the memory cap is the compiler’s self-appliedRLIMIT_AS, not a caller-sideulimit -v; the report path settled per-target, resolving Open Question 3 in the direction this document only leaned toward; and the seven-phase ledger it specifies is now misordered against the pipeline it models.Fourth, it recorded Phases 1–3 as design rather than as shipped code, so a reader could not tell what remained. The body below is a rewrite, not a patch. The legacy prose
Status:/Date:/Author:/Parent:header is deleted per SFEP-0001 §3; the parent relationships it asserted (SFEP-0006 §4.11, SFEP-0010) survive in §9.
1. Summary
An agent driving this repo must answer three questions from a failed build — what failed, where, and is a retry worth it — and a truncated tail of interleaved banners answers none of them. The fix, shipped since 2026-06, is a delimited, versioned verdict block emitted as the final lines of every agent-facing target, plus a per-target JSON report and a closed failure taxonomy that maps each class to a distinct correct response.
That contract is right and is in use. Its mechanism was the defect. The verdict
was assembled by a bash wrapper around make, which reconstructed pipeline state by
grepping another process’s human output. When #1502 moved the self-host stages
out of Makefile shell into sfn selfhost, the banners changed and the ledger died
without a single test going red.
This rewrite kept the contract and moved the host. The verdict became something
sfn produces rather than something bash infers: the maintainer verb
sfn dev verify owns the multi-phase pipeline, emits phase results from the
children’s own --json envelopes, and derives classification from exit status and
signals instead of regexes. The envelope was renamed sailfin-run/2 to retire the
make in its name alongside the Makefile. The bash detector got one interim
repair (Phase 5, SFN-724) so the window before the seed carried the native verb was
not a blind one, then was deleted with its host at cutover (Phase 7, SFN-726).
2. Problem
2.1 The original problem, unchanged
An agent invoking the toolchain sees a tail-truncated stream of interleaved shell banners and compiler text, and cannot reliably determine:
- What failed? A compile error, a test assertion, a non-deterministic IR mismatch, a setup error, an OOM, or a timeout — each demands a different response, and they look alike in a truncated tail.
- Where? Which phase, which file, which line. The full self-host pipeline runs
five top-level phases with four sub-phases inside
selfhost; a nonzero exit names none of them. - Is a retry worth it? An OOM under the 8 GiB cap or a known non-determinism flake should escalate or re-run; a real compile error must not be retried blind.
This framing is sound and is retained verbatim in intent. It is why the taxonomy in §3.7 is the load-bearing part of the design rather than the JSON shape.
2.2 The problem this rewrite adds
A verdict layer that reads its inputs by screen-scraping cannot survive refactoring of the thing it scrapes, and its tests will not tell you. The regression in §3.3 is not a stale regex to be corrected once. It is the predictable consequence of siting the reporter outside the process that holds the facts, coupled to a test that manufactures its own inputs. Repairing the regexes restores the feature; it does not remove the failure mode.
3. Design
3.1 Scope boundary — what this SFEP does not restate
| Territory | Owner |
|---|---|
The sfn check JSON envelope (sailfin-check/1), passes, incremental design |
SFEP-0004 |
BuildReport and --check-determinism; the build driver; Stage D Makefile retirement |
SFEP-0006 |
Test/hook syntax and the sfn/test capsule contract |
SFEP-0010 |
| Test-artifact caching and suite partitioning | SFEP-0011 |
| Runner performance internals | SFEP-0044 |
| Runner architecture | SFEP-0045 (Draft) |
Harness↔runner IPC; the --json jsonl schema |
SFEP-0050 |
The Diag/Span type, severity model, fix-it structure |
SFEP-0061 |
| The toolchain inventory and the envelope pattern as a cross-cutting rule | SFEP-0003 |
Per-job RAM budgeting and the RLIMIT_AS self-cap |
.claude/rules/compiler-safety.md |
sfn build --target= cross-compilation (which retires ci-cross-windows) |
SFEP-0021 |
This SFEP owns exactly one thing: the orchestration-layer verdict — the composite result of a multi-phase run, its failure classification, and the contract by which an agent reads it. Each tool’s own envelope is an input it composes, never a schema it redefines.
3.2 Shipped baseline
Phases 1–3 landed as code, not as design. Recorded here because an accurate inventory is the precondition for the rest of this document.
| Surface | State | Location |
|---|---|---|
===SAILFIN-RESULT=== verdict block |
Shipped, then re-hosted at Phase 7 | Originally emitted from the retired bash wrapper’s own EXIT trap; now emitted by compiler/src/cli/verdict.sfn::verdict_emit |
| Wrapping of agent-facing targets | Historical — retired at Phase 7 (SFN-726). Shipped, 9 targets | The <target>-impl split and its wrapping macro are deleted; <target>-impl recipes are folded back into <target>, and no make target produces a verdict any more |
| Nesting guard | Historical — retired at Phase 7 (SFN-726). Shipped | An env-var-based guard suppressed the inner sentinel when check invoked make test; deleted outright with the wrapper it guarded, since it addressed a Make-recursion shape nothing left in the tree still has |
| Schema document | Shipped, renamed at Phase 7 | docs/reference/run-result-schema.md (renamed from its prior filename) |
| Schema-lock tests | Historical — retired at Phase 7 (SFN-726). Shipped | Four e2e tests locking the bash producer and its ledger scraping were deleted with it; compiler/tests/e2e/dev_verify_test.sfn is the replacement schema lock |
| Per-target report file | Historical — retired at Phase 7 (SFN-726). Shipped | The 9-target build/agent-report.<target>.json composition lived in the retired wrapper; sfn dev verify’s analogous, always-on build/agent-report.verify.json is documented in docs/reference/run-result-schema.md |
| Failure taxonomy | Shipped, 7 classes | compiler/src/cli/verdict.sfn::verdict_classify — added crash beyond the six proposed, for signal-killed test children |
JSON=1 / SAILFIN_AGENT_REPORT=1 passthrough |
Shipped for the underlying tools’ own --json tee-ing (BuildReport, test jsonl, check-fast); the verdict-composition half retired with Phase 7 |
Makefile:955-981 (compile tees BuildReport), :360-425 (test), :758-767 (check-fast) |
Phase ledger for check |
Regressed, then superseded — see §3.3 | Repaired at Phase 5 (SFN-724), replaced outright by sfn dev verify’s structural ledger at Phase 6 (SFN-725) |
| Agent-facing surfacing (Phase 4) | Not started | no SAILFIN-RESULT reference under .claude/ or in CLAUDE.md; no sailfin_verify MCP tool |
Two corrections to this document’s own record:
- Open Question 3 is resolved. The report path is per-target
(
build/agent-report.<target>.json), not the singlebuild/agent-report.jsonthe original text proposed, so concurrent CI shards do not clobber. The implementation chose correctly; the design text was never updated. - Open Question 1 is resolved. The verdict block is always on. The report file
was gated behind
JSON=1under the bash producer;sfn dev verifywrites it unconditionally, since §3.8 makes it the durable record rather than an opt-in artefact. CI has run this shape since 2026-06 without objection.
3.3 The regression: the phase ledger is dead
Resolved by Phase 5 (SFN-724), then the whole layer retired by Phase 7 (SFN-726). The diagnosis below is kept as written — it is the record of how a scraping layer goes dark silently. What changed at Phase 5: the bash wrapper carried one
CHECK_PHASE_MARKERStable (fixed-string literals, real pipeline order, apass1-smokeentry) driving bothdetect_check_phaseandcompose_check_phases, andcheck_phase_ledger_test.sfnasserted every literal in it still appeared verbatim in its producer — so the next reword would fail the suite instead of going unnoticed. The “all live onmain” consequences below no longer were. The layer was still banner-scraping, and still interim: §5 Phase 6 (SFN-725) replaced it with a native verb that cannot drift, and Phase 7 (SFN-726) then deleted the wrapper outright —check_phase_ledger_test.sfnwas retired with it, replaced bycompiler/tests/e2e/dev_verify_test.sfn.
#1502 (design note docs/proposals/design-notes/1502-selfhost-check.md) moved the
stage2/stage3 build, viability smoke, and fixed-point diff out of ~90 lines of
Makefile shell into sfn selfhost (compiler/src/cli_selfhost.sfn). The new verb
prints its own [selfhost]-prefixed banners. The bash wrapper still grepped the
strings the Makefile had printed in June.
| The bash wrapper expected | Emitted at the time | Match |
|---|---|---|
[check] running test suite on first-pass binary |
[check] pass1 smoke gate: hello-world + sfn/test capsule tests (Makefile:686); running full suite on first-pass binary only under CHECK_FULL_PASS1=1 (:683) |
no |
proceeding to seedcheck build | verifying seed selfhost (stage2) |
[check] pass1 smoke passed — validating self-host (stage2/stage3 fixed point) (:699); [selfhost] building stage2 (seedcheck)... (cli_selfhost.sfn:578) |
no |
validating seedcheck binary can run programs |
[selfhost] validating seedcheck binary runs hello-world... (:610) |
no |
running test suite with seedcheck binary |
[check] running full test suite with seedcheck binary (cold backstop, ...) (Makefile:731) |
no |
building stage3 for fixed-point |
[selfhost] building stage3 (driven by seedcheck) for fixed-point check... (cli_selfhost.sfn:621) |
no |
comparing stage2 vs stage3 |
never printed | no |
[check][WARN] stage2 != stage3 |
[selfhost] stage2 != stage3: compiler output is not yet a fixed point (:668) |
no |
Consequences, all live on main at the time:
detect_check_phase()always falls through to its"compile"default (the bash wrapper, line 132), so everycheckfailure is attributed to the first phase regardless of where it occurred.compose_check_phases()marks the other six phasesskipped, so the report file asserts a run that did not happen.status: "warn"/failure: "nondeterminism"is unreachable. The[check][WARN] stage2 != stage3literal is its only trigger (:147-149) and no longer exists in any producer.
Why CI is green. compiler/tests/e2e/check_phase_ledger_test.sfn:94-114 drove
the bash wrapper with echo stubs that reproduce the June banners verbatim. The
test validates the classifier against a fossil of its own assumptions, so drift
between the classifier and the pipeline is structurally untestable. This is the
defect to fix first, ahead of the regexes: a test that manufactures its inputs
cannot detect that its inputs are wrong.
The ledger is also misordered. Independent of the string drift, the seven-phase
sequence this document specifies — … seedcheck-tests, stage3-build, fixed-point —
does not match the pipeline. sfn selfhost runs stage2 → smoke → stage3 →
fixed-point compare → promote as one internal chain (cli_selfhost.sfn:578,610,621,664),
and only then does check run the seedcheck test suite (Makefile:731-732). The
default pass1 step is a smoke gate, not the full suite, and the ledger has no entry
for it at all.
3.4 The host is being deleted
This document asserted that “the Makefile is explicitly sanctioned orchestration; reporting is orchestration.” That was true when written and is now the opposite of the plan of record:
- SFEP-0006 Stage D states “the Makefile is deleted (or shrunk to a 5-line
convenience wrapper)” (
0006:1509), with retirement explicitly deferred out of the Stage D PR that shipped everything else (:1533). Its design principle §2 is titled “No orchestration layer abovesfn” (:597-600). - The Makefile Retirement roadmap epic carries this at the
laterhorizon (site/src/data/roadmap.json:278-284), andSFN-60is the removal condition cited inline by every transitional shim in the tree (Makefile:531,562,compiler/src/cli/commands/dev_arena.sfn:7). - The native surface has already won on the merits.
sfn selfhost,sfn dev bootstrap fetch|build|check|pin|fingerprint|install,sfn dev clean|arena|shard|determinism-sweep, andsfn test|check|bench|packagecover nearly every target. What remains in the Makefile is sequencing, seed acquisition, andci-cross-windows.
Only two genuine capability gaps stood between the tree and Stage D. One is
sfn build --target=x86_64-w64-mingw32, which SFEP-0021 owns and which
Makefile:1113 already names as ci-cross-windows’s removal condition. The other
was this SFEP’s: nothing native sequenced compile → smoke → test → selfhost → test. That gap was why the verdict layer had been bash; closing it and rehosting
the verdict were the same piece of work, delivered by sfn dev verify (Phase 6,
SFN-725) with the cutover completed at Phase 7 (SFN-726).
3.5 The target host: sfn dev verify
A new leaf in the maintainer-only dev namespace — hidden from sfn --help,
alongside dev bootstrap|shard|arena|determinism-sweep|clean. sfn check is taken
by the typechecker and must not be overloaded; selfhost keeps its current narrow
meaning as the inner fixed-point validator this verb calls.
sfn dev verify [--fast] [--full-pass1] [--json]The phase ledger, corrected to the pipeline as it actually runs:
| Phase | Delegates to | Result source |
|---|---|---|
compile |
sfn dev bootstrap build |
BuildReport |
smoke-pass1 |
hello-world + sfn/test capsule gate |
test jsonl |
tests-pass1 |
sfn test — skipped unless --full-pass1 |
test jsonl |
selfhost |
sfn selfhost --json |
composite; splices that verb’s sub-envelope (stage2, seedcheck-smoke, stage3, fixed-point) |
tests-seedcheck |
sfn test against the seedcheck binary |
test jsonl |
--fast maps to today’s check-fast (sfn check compiler/src/ runtime/) and emits
a single-phase ledger.
The selfhost row’s sub-envelope did not exist before this SFEP: Phase 6 built it
as part of sfn dev verify rather than splicing an already-shipped surface —
before SFN-725, sfn selfhost --json printed only the determinism diff,
suppressed all phase structure, and emitted nothing at all on an early exit.
Two properties follow from siting this in sfn, and they are the whole point:
- Phase identity is structural, not textual. The verb dispatches each phase, so
it knows which one is running without inferring it. The
#1502class of drift becomes impossible — a refactor that renames a banner cannot desynchronize a ledger that never read the banner. - Sub-phases compose rather than flatten.
sfn selfhost --jsonemits its own envelope;verifysplices it in. Each producer describes only what it owns, which is the SFEP-0003 §3.3 envelope pattern applied recursively instead of a supervisor re-deriving four phases it does not run.
Composition, not reimplementation. verify spawns the same children the
Makefile does today and adds no compilation logic. This keeps it inside the
observability carve-out in §4 and out of
.claude/rules/selfhost-invariant.md’s prohibition on build-driver fixups.
3.6 The envelope: sailfin-run/2
The envelope’s prior make-hosted generation is renamed and bumped in one coordinated break.
===SAILFIN-RESULT==={"schema_version":"sailfin-run/2","host":"sfn dev verify","target":"verify","status":"fail","failure":"test-failure","phase":"tests-seedcheck","first_error":"compiler/tests/unit/foo_test.sfn:42","report":"build/agent-report.verify.json"}===END-SAILFIN-RESULT===| Field | Change from the prior make-hosted generation |
|---|---|
schema_version |
"sailfin-run/2". Renamed; the number stays monotonic across the rename so a consumer that has seen the prior generation cannot mistake sailfin-run/2 for an earlier one. |
host |
New. The invocation that produced the verdict (sfn dev verify, or make check during the transition). Lets one consumer read both hosts across the cutover. |
target, status, failure, phase, first_error, report |
Unchanged in meaning. phase now draws from the corrected ledger in §3.5. |
Consumers requiring coordination: the schema doc (renamed to
run-result-schema.md), make_result_contract_test.sfn,
make_report_contract_test.sfn, check_phase_ledger_test.sfn, and SFEP-0003’s
five-envelope inventory (0003:42,235). SFEP-0003 is accurate today and should be
amended at cutover, not ahead of it.
Rename rather than freeze, because the shape genuinely changes: phases become
producer-emitted, first_error becomes structured, and host is added. The pattern
requires consumers to hard-fail on an unknown version (SFEP-0003 §3.3 point 4),
which is exactly the mechanism for making that visible.
3.7 Classification becomes producer-emitted
The closed set is the reason this SFEP exists — distinct classes drive distinct responses. The taxonomy is unchanged from what shipped; what changes is that every class stops being inferred from text.
failure |
status |
Derived natively from | Agent’s correct response |
|---|---|---|---|
compile-error |
fail |
BuildReport / sailfin-check/1 diagnostics, in-process |
Read diagnostics, fix source — do not retry |
test-failure |
fail |
test jsonl summary.failed > 0 |
Read the failing test’s event |
nondeterminism |
warn |
sfn selfhost fixed-point result — a return value, not a banner |
Re-run once; if it persists, seed-stabilizer |
setup-error |
fail |
child exit 2, per the SFEP-0003 §3.3 point 6 convention |
Fix invocation/env, not source |
oom |
fail |
child killed at the RLIMIT_AS self-cap; exit 137 / signal |
Escalate as a memory regression — do not blind-retry |
timeout |
fail |
supervisor-owned deadline; exit 124 |
Re-run or escalate per phase |
crash |
fail |
fatal signal (139/135/136/132), excluding 134 — a clean assert also aborts 134 |
Escalate; a fault is never a flake |
nondeterminism remains the sole class paired with status: "warn" and exit 0,
because a stage2 ≠ stage3 mismatch is a signal the pipeline deliberately does not
treat as fatal. The verdict must surface it without flipping the exit code.
Every row’s source is a number or a struct field. That is the substantive
improvement: exit status and signals are already structured data that the current
design throws away in favour of grepping for Segmentation fault in a log.
3.8 Durability — who reports when the reporter dies
The one real merit of the bash wrapper: as an outer process, it survives the death
of the tool it wraps. Moving the verdict inside sfn must not lose that.
- A phase child dying is strictly better handled natively.
verifyis a thin supervisor spawning each phase as a child (the architecturesfn selfhostalready uses atcli_selfhost.sfn:444, and the test pool uses via the SFN-402 process handles). A child hitting the 8 GiB cap or taking a signal is reported to the supervisor as an exit status — the case §3.7 now reads structurally instead of by regex. - The supervisor’s own death is the residual case, and only from the host OOM
killer, since the supervisor’s footprint is small and its
RLIMIT_ASis per-process. This is unanswerable in-process, so the report file becomes the durable record:build/agent-report.<target>.jsonis written incrementally, appending each phase result as it completes, with"complete": falseuntil the verdict lands. An agent that gets no sentinel reads the file and sees the last completed phase plus the one in flight.
This was a net gain over the retired bash wrapper, where write_report_file was
called only from inside its EXIT trap, so a SIGKILL of the process group lost
the verdict and the report together.
3.9 Relationship to the SFEP-0003 envelope pattern
SFEP-0003 §3.3 point 3 requires a human rendering and a machine envelope to be
mutually exclusive under --json. The verdict block is deliberately additive
— always printed, including in human mode. This is not a violation, and the
distinction is worth stating so nobody reads it as one:
- The mutual-exclusivity rule governs a tool’s primary output document, which
must be parseable as a whole.
sfn dev verify --jsonobeys it. - The verdict block is a framed trailer, delimited precisely so it can coexist with arbitrary preceding output. Consumers read the last occurrence of the sentinel. Its value comes from surviving truncation, which requires it to be present in human mode — the mode agents actually get when a phase fails noisily.
4. Non-goals
- Changing any compiler-correctness behaviour. Reporting only.
- Redesigning the per-tool
--jsonschemas.sailfin-check/1,BuildReport, and the test jsonl are inputs to compose, owned elsewhere (§3.1). - Replacing human output. Banners stay; the verdict is additive and last.
- Retiring the Makefile. That is SFEP-0006 Stage D. This SFEP closes one of its two remaining capability gaps and must not grow into the sweep itself.
ci-cross-windows. Waits onsfn build --target=(SFEP-0021).- A public
sfn verify. The phases (seedcheck,fixed-point) are meaningless outside this repo; a downstream-facing verification gate is a separate question.
5. Phasing
Phases 1–3, 5, 6, and 7 are historical and shipped except where noted. Phase 4 was deliberately resequenced behind the cutover: telling every agent to read a sentinel whose host and schema version were about to change would have taught one contract only to retract it. That gate cleared at Phase 7 (SFN-726); Phase 4 remains unstarted because nobody has picked it up yet, not because it is blocked.
| Phase | Size | Status | Scope | Deliverable |
|---|---|---|---|---|
| 1 — keystone | S | Shipped, then retired at Phase 7 | Makefile, the retired bash producer, docs/reference/run-result-schema.md (renamed from its prior filename) |
Always-last ===SAILFIN-RESULT=== block on 9 targets, with status + failure |
| 2 — full report | M | Shipped | JSON=1 passthrough; per-target report composition |
build/agent-report.<target>.json with a phases[] array |
| 3 — taxonomy + first-error | S | Partial | classify(); schema lock |
Taxonomy shipped (7 classes). Phase ledger regressed — §3.3 |
| 5 — interim ledger repair | S | Shipped (SFN-724) | check_phase_ledger_test.sfn first, then the bash producer’s detectors |
Fix the test before the regexes. Replace the echo-stub fixtures with a marker-presence assertion: every literal the detectors grep must appear verbatim in a producer (Makefile or cli_selfhost.sfn). Static, milliseconds, and it would have failed on #1502 — where driving a real 15–20 min make check from a test is unaffordable and a recorded transcript would become the next fossil. Then re-sync detectors to the [selfhost] banners and the corrected phase order, restoring the nondeterminism warn path. Closes the blind window before a seed carries Phase 6. |
| 6 — native verb | M | Shipped | new cli/commands/dev_verify.sfn; sfn selfhost --json sub-envelope; incremental report writes |
sfn dev verify [--fast] [--full-pass1] [--json] runs the pipeline and emits the verdict from child envelopes and exit status. Ledger and classification per §3.5/§3.7. Ships with the Phase 5 test repointed at the native verb. |
| 7 — cutover | M | Shipped (SFN-726) | rename schema doc; bump tests; delete the bash producer and its Makefile wrapping; amend SFEP-0003 §3.2 |
sailfin-run/2 is the sole envelope, produced only by sfn dev verify. make check was not re-plumbed onto sfn dev verify — it keeps its own pipeline and emits no verdict at all now — because that rehost is SFN-60 (Makefile retirement), out of this SFEP’s scope; see the resolved Open Question 1. |
| 4 — surfacing | S | Not started, gated on 7 | CLAUDE.md + .claude/agents/*; sailfin_verify MCP tool; llms.txt |
Agents are told to read the sentinel. MCP clients get it as structuredContent — which also closes the gap where sailfin_build and sailfin_test pass --json through as raw text (tools/mcp-server/src/index.ts:388-447) while sailfin_diagnostics parses it properly. |
Phase 6 is seed-gated. Like SFN-679 and SFN-680, sfn dev verify is executed
by the pinned seed, so it is only exercisable once a seed carrying it is pinned
(docs/status.md:124-129,150-152). It is a compiler-source capability with a
consumer in the same tree, so per .claude/rules/seed-dependency.md it bundles with
its consumer and does not justify an off-cadence seed cut. Phase 5 exists
precisely so the intervening window is observable.
6. Risks
- (medium) Phase 5 looks like wasted effort on a doomed layer. It is one small
diff, and its durable half is the test — repointing
check_phase_ledger_test.sfnat real output is what makes Phase 6 verifiable and is not thrown away at cutover. Without it, the tree has no working phase ledger for the whole seed-cadence window, and no test capable of noticing. - (medium)
verifyaccretes build logic. A supervisor that starts “fixing up” a phase becomes the build-driver fixup.claude/rules/selfhost-invariant.mdforbids. Mitigation:verifymay only spawn children, read their envelopes, and report. Any behaviour change belongs in the phase’s own command. - (low) Envelope rename churn. Five consumers, all in-tree, all schema-locked — so a missed one fails CI rather than silently degrading. SFEP-0003 is amended at cutover, not before.
- (low) Sentinel collision. A test printing the sentinel would confuse a naive grep. Consumers read the last occurrence; the schema-lock test guards the exact delimiter.
- (low) Double-counting nested runs. An env-var-based guard handled this for the
retired
check→make testnesting; the native verb needs no equivalent — none of the children it spawns (sfn test,sfn build,sfn dev bootstrap build) emit a verdict trailer natively, so onlysfn dev verifyitself ever produces one. - (none) Self-hosting. Phases 1–3 and 5 touch no
compiler/src. Phase 6 does, and clears the full Stage1 readiness gate like any other compiler change.
7. Success metrics
Falsifiable, and stated so the §3.3 failure could not be recorded as success:
- An agent determines
{host, target, status, failure, phase, first_error}for any agent-facing invocation from the last 5 lines of output, with zero upthread scrolling. failuredistinguishes all seven classes, and each maps to a distinct documented response.- A failing phase is named correctly, and never verified by replaying expected
banners into the classifier. The guard is tiered to what each host can afford:
Phase 5 asserts every grepped marker exists verbatim in a producer (static, and
sufficient to catch the
#1502drift); Phase 6 injects a real failure into each of the five phases and asserts the reportedphasematches — which is affordable precisely because the native verb dispatches the phases it reports on. - A refactor of a phase’s human output cannot change the reported
phase. This is the invariant#1502violated, and it is the one metric the bash mechanism cannot satisfy at any level of regex care. - A hard kill of the supervisor still leaves the last completed phase readable in
build/agent-report.<target>.json. - Zero
compiler/srcchange in Phases 1–3 and 5.
8. Open questions
- Does
make checkbecome a wrapper or disappear? Resolved at Phase 7 (SFN-726): neither, yet.make checkwas not re-plumbed ontosfn dev verify— it keeps sequencing its own pipeline directly, and since Phase 7 deleted the bash producer and theMakefile’s<target>-implwrapping, it emits no verdict of any kind. Re-plumbingmake checkontosfn dev verify(or deleting the target outright) isSFN-60(Makefile retirement), which this SFEP does not own. - Should the CI shard legs consume
sailfin-run/2directly?build-quality.ymlalready gates onBuildReport’scache.hit_rate(.github/workflows/build-quality.yml:250-256). A composed run envelope could replace several bespoke log greps in CI, but that is a CI refactor with its own blast radius, not part of this SFEP. - Does
--fastbelong onverifyat all, or doescheck-fastsimply become the already-sufficientsfn check compiler/src/ runtime/with no wrapper? The verdict block is the only thing the wrapper adds; ifsfn checkgrows the sentinel itself,--fastis unnecessary. - Where does the residual shell surface go?
scripts/check-examples.shandscripts/corpus-run.shboth emit ad-hoc machine-readable summaries (corpus-run.sh:33cites this SFEP as its pattern) and both need an XFAIL/XPASS ratchet concept no native command has. Candidate follow-on, explicitly out of scope here.
9. References
docs/proposals/0003-tooling.md— the toolchain surface; the envelope pattern (§3.3) and the five-envelope inventory this SFEP’s rename touchesdocs/proposals/0006-build-architecture.md— §4.11 structured link diagnostics (this SFEP’s original parent); Stage D Makefile retirement (:1509,1533); “No orchestration layer abovesfn” (:597-600)docs/proposals/0010-test-infra.md— test/hook syntax and thesfn/testcapsule contractdocs/proposals/0021-windows-native-selfhost.md—sfn build --target=, which retiresci-cross-windowsdocs/proposals/0050-streamed-test-ipc.md— harness↔runner IPC; the--jsonv2 schema is unchanged by itdocs/proposals/design-notes/1502-selfhost-check.md— the refactor that broke the ledgerdocs/reference/run-result-schema.md— the shippedsailfin-run/2contract, renamed from itsmake-hosted predecessor at Phase 7.claude/rules/seed-dependency.md— why Phase 6 bundles rather than splits.claude/rules/compiler-safety.md— theRLIMIT_ASself-cap behind theoomclass