Spec onboarding
v7 ships a typed SpecOnboardingMethod in Haft Core. The method
is product knowledge: it declares which YAML fields a SpecSection must carry, in
which order, with which structural Checks. Surfaces (MCP plugin and CLI
subcommand; an alpha desktop wizard exists outside the production envelope) all
consume the same WorkflowIntent shape — they render and dispatch,
they do not invent semantics.
The method is implemented in internal/project/specflow/. Phases are
declarative Go values composed from a reusable vocabulary of typed Checks
(RequireField, RequireStatementType,
RequireClaimLayer, RequireValidUntil,
RequireTermDefined, RequireGuardLocation,
RequireBoundaryPerspectives). The same Check vocabulary feeds the
parser-level haft spec check, so forward (method) and backward
(validator) directions never drift.
The typed flow
Surfaces ask Core for the next step:
haft_spec_section(action="next_step") # MCP plugin
haft spec onboard --json # CLI The response is a WorkflowIntent:
| Field | Meaning |
|---|---|
phase | Stable id, e.g. target.environment.draft. Do not skip ahead. |
prompt_for_user | Question the operator answers verbatim. |
context_for_agent | Reasoning context for the host agent. Names which FPF patterns to apply. |
expected_fields | YAML keys the new SpecSection MUST carry. |
checks | Structural validators that will run on the result. |
blocking_findings | Non-empty when an existing section fails validation. Resolve before drafting. |
terminal | true when every registered phase is satisfied. |
Phase registry
v7 ships ten phases: three target-system, seven enabling-system. Order matters —
each phase declares its depends_on, and Core advances only when
dependencies are satisfied with active baselined sections.
Target-system spine
target.environment.draft— what changes in the world when the target system runs. Apply FRAME-09 + CHR-12 in reasoning.target.role.draft— what role the target plays in producing the environment change. FRAME-09 strict distinction quad.target.boundary.draft— in-scope vs out-of-scope, plus four CHR-10 boundary perspectives (Law / Admissibility / Deontics / Evidence) named intarget_refs.
Enabling-system spine
enabling.architecture.draft— layered architecture of the team / code / infra that builds and operates the target. Apply FPF Signature Stack.enabling.work_methods.draft— how each load-bearing artifact is produced. FRAME-09 + X-STATEMENT-TYPE.enabling.effect_boundaries.draft— which actors and surfaces may mutate what. CHR-10 boundary corners.enabling.agent_policy.draft— supported host agents and autonomy bounds.enabling.commission_policy.draft— how WorkCommissions are authorized, scoped, retired.enabling.runtime_policy.draft— which surface owns runtime lifecycle, isolation, observability.enabling.evidence_policy.draft— admissible evidence kinds, minimum congruence level, refresh triggers.evidence_required[].kindnames a guard location.
The phase loop
- Call
haft_spec_section(action="next_step"). - Read
prompt_for_userverbatim to the operator. - Read
context_for_agent. Retrieve every named FPF pattern viahaft_query(action="fpf", query="FRAME-09")etc. Apply them in your reasoning — never inside the YAML. - Read repo carriers (README, manifests, source structure) to ground the section in reality.
- Draft the YAML
spec-sectionblock in the carrier file named bydocument_kind. Populate every field inexpected_fields, including the SoTA fields below. - Run
haft spec check. Resolve any structural finding. - Promote to active: flip
status: draft → activeafter the operator approves the load-bearing claims. - Record the baseline:
haft_spec_section(action="approve", section_id=<id>). Without a baseline the section reportsspec_section_needs_baselineand the next phase stays blocked. - Repeat from step 1 until
terminal: true.
SoTA fields on every claim
statement_type— one ofdefinition,admissibility,duty,evidence,explanation. Mixed types are an L1 error per X-STATEMENT-TYPE; decompose.claim_layer— one ofobject,description,carrier,work,evidence.valid_until— RFC3339 or YYYY-MM-DD. Refresh discipline lives at the claim level, not only at evidence.target_refs— for boundary sections, enumerate at least four stakeholder perspectives (CHR-10 corners).evidence_required[].kind— for invariants and illegal states, declare guard location:type,L1..L4,DB,E2E,manual.
Drift, staleness, and triage
Once a section is approved (baseline recorded), hand-edits to the carrier surface
as spec_section_drifted in haft spec check. An active
section whose valid_until is past today surfaces as
spec_section_stale. The operator picks one of three actions:
# Intentional evolution: lock the new hash with a recorded reason
haft_spec_section(action="rebaseline", section_id="TS.role.001",
reason="role narrowed after stakeholder review")
# Section needs review: drop the baseline, return to the onboarding loop
haft_spec_section(action="reopen", section_id="TS.role.001",
reason="external dependency changed")
# Mistaken edit: revert the YAML in the carrier; haft spec check goes clean What never goes into the carrier
- FPF citations (
FRAME-XX,CHR-XX,X-XXX). Patterns shape your reasoning; the carrier records the resolved claim only. - Speculative fields the phase did not request.
- Active status without an explicit operator approval.
Plugin-mode parity
The CLI haft spec onboard --json output and the MCP
haft_spec_section(action="next_step") response share an identical
WorkflowIntent shape. A contract test asserts field-by-field parity.
Whatever the host agent sees in plugin mode, the operator sees in the CLI.