Slartibartfast Engine — Plan-Architect

Status: Implemented (M0–M6). This spec describes the actual implementation status. The original pre-implementation sketch (Template-Selection + Slot-Filling) was discarded — the actual implementation is an evidence-based phased workflow with hard validation gates. See §2.

Naming Note: In the Adams universe, Slartibartfast is the planet designer who won an award for the Norwegian fjords — an architect with a love for structured detail. Exactly the role of this Engine.

1. Role and Classification

Slartibartfast generates executable Plans (Recipes) from a free user description. Input: description + output schema type (see §4). Output: a parser-validated Recipe YAML, persisted as a Document, plus a complete audit chain (which assumptions, which evidence, which subgoals, which LLM calls).

Default: Slartibartfast plans AND executes. After PERSISTING, the Engine spawns the generated Recipe itself as a Child-Process, waits for its ProcessEvent, checks the produced artifacts against the Acceptance-Criteria (EXECUTION_VALIDATING + ContentValidatingPhase) and only then concludes with DONE. If artifacts are missing/too small, a recovery loop back to PROPOSING is triggered. With planOnly=true (Engine parameter, see §6), Slartibartfast stops after PERSISTING and leaves the execution to the caller.

Identity Feature — the only LLM-driven write path into the Project Configuration. Other Engines write outputs within a Project (Documents, Tasks, Chat-Replies); they do not change Recipes, Skill frontmatter, or Settings. Kits also write to the Project configuration, but deterministically from a Git bundle and explicitly triggered by the user. Slartibartfast is the only place where the Project architecture (Recipes, strategies) is created or grown through an LLM dialogue. Hactar v2 is the corresponding Script-Execution-Engine (Phase 3 of the Split-Refactor, see planning/script-architect-executor-split.md): no authoring, only loading + validating + executing sandboxed JS. Thus, Slart is the only LLM-driven authoring engine in Vancetope — JavaScript scripts (outputSchemaType=SCRIPT_JS) now belong to Slart’s output family, not Hactar’s.

Engine Mental Model Use-Case Class
Vogon Strict Workflow — Plan upfront, execute deterministically “Implement Feature X using this procedure with gates”
Marvin Task-Decomposition — Tree grows through NEEDS_SUBTASKS “Break this down into manageable pieces”
Trillian Goal-Anchored Refinement — iterative approximation “Goal = X, find the path and adapt it”
Hactar (v2) Pure Script-Executor — no LLM, only load + validate + execute “Run this script with these args”
Slartibartfast Evidence-Based Authoring: Goal + Manuals + Reasoning → Recipe-YAML OR Script (+ optional Self-Execute) “Generate the workflow / script for this task — and ideally execute it immediately”

When Slartibartfast instead of Trillian? When the task fits into a conclusive plan form that a downstream Engine (Vogon/Marvin/Zaphod) can execute. Trillian is the open variant for long-running refinement tasks without a fixed endpoint.

2. Phased Workflow

Slartibartfast is a state-machine-based Engine with 12 lifecycle phases (10 plan phases + 2 execute phases when planOnly=false). Each phase is a Spring @Component under vance-brain/src/main/java/de/mhus/vance/brain/slartibartfast/phases/, operates on the common ArchitectState structure, and explicitly records its audit trail (PhaseIteration, Rationale, LlmCallRecord). Each LLM phase has a hard re-prompt loop with concrete validation hints.

READY
  ↓
FRAMING            LLM: User-Text → FramedGoal with
                   statedCriteria (USER_STATED) + assumedCriteria
                   (INFERRED_CONVENTION/DOMAIN/CONTEXT, with
                   confidence + rationale).
  ↓
CONFIRMING         Pure logic: stated + high-conf assumed →
                   acceptanceCriteria. Low-conf assumed according to
                   confirmationMode (DROP_LOW_CONF | KEEP_ALL |
                   ASK_LOW_CONF). For ASK_LOW_CONF: Inbox dialogue,
                   Engine parks.
  ↓
GATHERING          Tool calls via DocumentService:
                   all manuals/-Documents are read as
                   EvidenceSources, with
                   gatheringRationaleId per Source.
  ↓
CLASSIFYING        One LLM call per EvidenceSource: extracts
                   atomic Claims, classifies each as
                   FACT / EXAMPLE / OPINION / OUTDATED. Non-FACT
                   Claims carry classificationRationaleId.
  ↓
DECOMPOSING        ◄─────────┐  Recovery loop on BINDING fail
                   LLM: From │  (max maxRecoveries, default 5)
                   acceptanceCriteria + Claims, Subgoals are created,
                   each Subgoal evidence-tied (evidenceRefs to
                   Claim IDs) OR speculative=true with Rationale.
                   decompositionRationaleId for the plan form.
  ↓                          │
BINDING            Hard validator gate (6 rules):              │
                   - each Subgoal has evidence OR is speculative
                   - claim-refs resolve                         │
                   - criterion-refs resolve                     │
                   - non-speculative cite at least 1 FACT/EXAMPLE
                   - each acceptanceCriterion is addressed by ≥1
                     Subgoal (Coverage)                         │
                   - speculation-ratio ≤ maxSpeculativeRatio
                   On Fail: pendingRecovery → DECOMPOSING ─────┘
  ↓ (pass)
PROPOSING          ◄─────────┐  Recovery loop on VALIDATING fail
                   LLM call with System-Prompt switched to
                   outputSchemaType:
                   - VOGON_PLAN: generates Recipe YAML with
                     engine: vogon + inline strategyPlanYaml
                   - MARVIN_RECIPE: generates Recipe YAML with
                     engine: marvin + params + promptPrefix
                   Delivers RecipeDraft with yaml + justifications-
                   Map (constraint-key → sg-id) + shapeRationale.
  ↓                          │
VALIDATING         Hard validator gate (6 rules):              │
                   - YAML parses + is Mapping                   │
                   - recipe.name + recipe.engine present        │
                   - VOGON_PLAN: embedded strategyPlanYaml
                     parses via StrategyResolver
                   - MARVIN_RECIPE: promptPrefix non-blank +
                     params block present
                   - justification refs resolve to subgoal IDs  │
                   On Fail: pendingRecovery → PROPOSING ───────┘
  ↓ (pass)
PERSISTING         DocumentService writes
                   recipes/_slart/<runId>/<recipe-name>.yaml
                   (see §8) + sibling audit.json with complete
                   ArchitectState. Builds TerminationRationale.
  ↓
                   (planOnly=true: → DONE; else → EXECUTING)
  ↓
EXECUTING          ◄─────────┐  Recovery loop on
                   RecipeResolver loads the Recipe written in PERSISTING,
                   ThinkEngineService spawns a Child with the target Engine (Vogon /
                   Marvin / …). Slart's Process parks BLOCKED
                   until the Child's ProcessEvent arrives.
                   On Child-DONE → EXECUTION_VALIDATING. On
                   Child-FAILED/STOPPED → FAILED.
  ↓                          │   EXECUTION_VALIDATING fail
EXECUTION_VALIDATING          │
                   Pure logic (regex on Subgoal texts): extracts
                   expected file paths from non-speculative
                   Subgoals, checks via DocumentService.findByPath
                   if each path exists + has ≥200 characters.
                   Then optional ContentValidatingPhase
                   (LLM-Judge against User-Criteria), if any
                   are set.
                   On Fail: pendingRecovery → PROPOSING ───────┘
                   with detailed hint (what's missing, what
                   remains, phase-add/phase-extend suggestions).
  ↓ (pass)
DONE

Recovery branches at BINDING, VALIDATING, and EXECUTION_VALIDATING collectively count against maxRecoveries. Upon exhaustion: according to escalationMode, either directly ESCALATED or via ESCALATING → Inbox dialogue → User decides.

LLM Hardening per Phase (Pattern): SystemPrompt with “EXACTLY one JSON object”, “no Markdown wrapper”. Output schema strictly checked. On schema violation: re-prompt with concrete validator output as hint. Max 2 corrections per LLM call.

3. ArchitectState

Persisted on ThinkProcessDocument.engineParams.architectState. Single source of truth for the audit chain. Most important fields:

runId                      "3a4f7c91"  — 8-hex UUIDv4-prefix, assigned once
                                        at spawn, storage
                                        bucket key
userDescription            verbatim user text
outputSchemaType           VOGON_PLAN | MARVIN_RECIPE | ZAPHOD_RECIPE | SCRIPT_JS | MAGRATHEA_WORKFLOW | BENJY_RECIPE
mode                       CREATE | EDIT | UPDATE — drives the
                                        invent vs. patch branch, the
                                        LOADING_EXISTING phase, and the
                                        PERSISTING write-path
existingScriptRef          String — SCRIPT_JS UPDATE only; document
                                        path of the existing script
existingScriptCode         String — loaded body for UPDATE mode
                                        (filled by LOADING_EXISTING)
priorFailureReason         String — UPDATE-mode optional context from
                                        a prior Hactar-FAILED run
status                     ArchitectStatus enum
goal                       FramedGoal { framed, sourceUserText,
                                        statedCriteria[], assumedCriteria[] }
acceptanceCriteria         Criterion[] — Output from CONFIRMING
evidenceSources            EvidenceSource[] — Output from GATHERING
evidenceClaims             Claim[] — Output from CLASSIFYING
subgoals                   Subgoal[] — Output from DECOMPOSING
decompositionRationaleId   Rationale-Ref for the plan form
proposedRecipe             RecipeDraft — Output from PROPOSING
rationales                 Rationale[] — append-only Pool, each
                                        phase adds its justifications
iterations                 PhaseIteration[] — one entry per phase run,
                                        audit history
llmCallRecords             LlmCallRecord[] — one entry per LLM call
                                        (auditLlmCalls=true)
validationReport           ValidationCheck[] — last gate results
pendingRecovery            RecoveryRequest — set by
                                        BINDING/VALIDATING on fail
recoveryCount              int — total recoveries (BINDING +
                                        VALIDATING combined)
maxRecoveries              int (default 5)
confirmationThreshold      double (default 0.85)
maxSpeculativeRatio        double (default 0.30)
auditLlmCalls              boolean (default true)
confirmationMode           ConfirmationMode (see §6)
escalationMode             EscalationMode (see §6)
pendingInboxItemId         String — InboxItem ID the Engine
                                        is currently waiting for (see §7)
pendingInboxKind           CONFIRMATION | ESCALATION | NONE
terminationRationale       TerminationRationale — set by
                                        PERSISTING
persistedRecipePath        recipes/_slart/<runId>/<name>.yaml
failureReason              String on FAILED

All structures in vance-api/.../slartibartfast/, Lombok builder, Jackson roundtrip-stable.

VOGON_PLAN was called VOGON_STRATEGY, and the @JsonAlias for it remains. It is not a cleanup remnant, but what made the renaming survivable: a Slart Process that was running at deploy time carries the old name persisted in its engineParams, and loadState deserializes this state on every subsequent turn. Without the alias, Jackson rejects it and the Process is permanently stuck — the lenient fallback for spawn parameters never sees the persisted state.

Audit Chain Invariant

Every non-trivial artifact references another by ID:

EvidenceSource.gatheringRationaleId        → Rationale.id
Claim.sourceId                             → EvidenceSource.id
Claim.classificationRationaleId            → Rationale.id (non-FACT only)
Criterion(assumed).rationaleId             → Rationale.id (INFERRED_*)
Subgoal.evidenceRefs[]                     → Claim.id
Subgoal.criterionRefs[]                    → Criterion.id
RecipeDraft.shapeRationaleId               → Rationale.id
RecipeDraft.justifications[constraint-key] → Subgoal.id
PhaseIteration.llmCallRecordId             → LlmCallRecord.id
TerminationRationale.criterionCoverage[c]  → Subgoal.id[]

Validators (BINDING, VALIDATING) check referential integrity and demand re-generation for dangling refs.

4. Output Schema Types

Which schema types Slartibartfast can generate. The set is additively extensible — each schema type carries a schema-specific system prompt (for PROPOSING) and a schema-specific parser (for VALIDATING); the rest of the lifecycle (FRAMING, GATHERING, CLASSIFYING, DECOMPOSING, BINDING, PERSISTING, EXECUTING, EXECUTION_VALIDATING) is schema-agnostic.

Schema Type Status Validator (in VALIDATING) Spawn Engine
vogon-strategy production VogonArchitectStrategyResolver.parseStrategy + worker-recipe-existence-Check Vogon
marvin-recipe production MarvinArchitect — promptPrefix non-blank + Pebble-Template-Compile + params-Map + allowedSubTaskRecipes/recipesOnlyViaExpand resolve via RecipeLoader Marvin
zaphod-recipe production ZaphodArchitectZaphodHeadsParser.parseRecipe (mirrors ZaphodEngine.buildInitialState validation) Zaphod
script-js production JsScriptArchitect — delegates to HactarService.validate(...) (parse + JSDoc-Header + Tool Allowlist) Hactar (via DirectExecutionSpawn)
magrathea-workflow production MagratheaArchitect — delegates to MagratheaWorkflowLoader.validateYaml(...) (State-Machine-Parse) + agent_task.recipe-existence-Check none (author-only, planOnly)
benjy-recipe production BenjyArchitect — delegates the params shape to the engine’s own fail-fast (BenjyFeatureConfig.fromParams, no parallel validator schema) + RecipeLoader-resolve of each reference (Doer, Controller-Profile, Escalation) + Kind-Check (Controller = internal: true-LightLm-Profile, Doer/Escalation = spawnable Workers) Benjy (author-only — the bundled benjy-architect recipe sets planOnly: true)

Schema-specific knowledge lives in SchemaArchitect beans under de.mhus.vance.brain.slartibartfast.architect.*. The lifecycle phases (ProposingPhase, ValidatingPhase, PersistingPhase) are schema-agnostic and resolve the Architect via Map<OutputSchemaType, SchemaArchitect>. New schema type ⇒ new bean, no edits in the phase classes.

Recipe Schemas vs. Script Schemas: vogon-strategy, marvin-recipe, zaphod-recipe, benjy-recipe produce Recipe YAML (persisted under _vance/recipes/_slart/<runId>/<name>.yaml, EXECUTING goes through the RecipeResolver). script-js produces JavaScript (persisted under _vance/scripts/_slart/<runId>/<name>.js, EXECUTING spawns Hactar directly via architect.directExecutionSpawn(...) — see SchemaArchitect.DirectExecutionSpawn). VALIDATING skips the recipe-specific YAML parse + engine:- field checks for script-js (controlled by architect.isRecipeOutput()).

magrathea-workflow produces a workflow document (state machine, NOT a Recipe — no engine: field). Magrathea is a workflow orchestration subsystem, not a ThinkEngine that Slart could spawn as a child and wait for a terminal ProcessEvent. The MagratheaArchitect is therefore author-only: isRecipeOutput()=false (VALIDATING skips the recipe- specific checks), persistsAtFlatPath()=true (PERSISTING writes directly to _vance/workflows/<name>.yaml — the path that the MagratheaWorkflowLoader resolves, so immediately startable via workflow_start, instead of in the _slart-sandbox bucket). The bundled magrathea-architect recipe sets params.planOnly: true — the run ends after PERSISTING with DONE, without EXECUTING/EXECUTION_VALIDATING. Executing the workflow is a separate step (workflow_start tool, scheduler, or REST). The Architect is just a bean when vance.services.magrathea=true (like the Loader).

MARVIN_RECIPE Output Form:

name:        <recipe-name>
description: |
  <description>
engine: marvin
params:
  rootTaskKind: PLAN
  maxPlanCorrections: 2
  # optional Marvin-Constraints (from Phase M-Q)
  allowedSubTaskRecipes: [...]
  recipesOnlyViaExpand: [...]
  allowedExpandDocumentRefPaths: [...]
  requiredChildTemplateRecipeParams: { ... }
  disallowedTaskKinds: [AGGREGATE]
  defaultExecutionMode: SEQUENTIAL | PARALLEL
promptPrefix: |
  You are the <name>-PLAN node.
  Generate EXACTLY N Children: ...

Slartibartfast matches shape plus sub-recipe existence: each name in allowedSubTaskRecipes / recipesOnlyViaExpand must resolve via the Project RecipeLoader, otherwise MarvinArchitect-VALIDATING rejects the Recipe and drives re-PROPOSE.

VOGON_PLAN Output Form:

name:        <recipe-name>
description: ...
engine: vogon
params:
  strategyPlanYaml: |
    name: <strategy-name>
    version: "1"
    phases:
      - name: <phase>
        worker: <recipe or ford>
        workerInput: |
          <prompt>
        gate: { requires: [<phase>_completed] }

ZAPHOD_RECIPE Output Form:

name:        <recipe-name>
description: ...
engine: zaphod
params:
  pattern: COUNCIL
  heads:
    - name: <unique kebab-case head-name>
      recipe: <recipe-name from project, typically ford>
      persona: |
        <distinct perspective / bias / role>
    - ...
  synthesisPrompt: |
    <instruction for the synthesizer turn>

Council shape rules (validated by ZaphodHeadsParser): 2-5 Heads sweet-spot, hard cap at ZaphodEngine.MAX_HEADS, unique Head names, each Head references a Project Recipe. The appendProposingContext of the Architect provides the Slart LLM with the Project Recipe list, so that Head Recipes are not hallucinated.

SCRIPT_JS Output Form (JavaScript Orchestrator Script, NOT Recipe YAML):

/**
 * @description <one-liner>
 * @timeout 30m
 * @statements 10M
 * @requiresTools imap_fetch, light_llm_call, inbox_create
 * @allowTools imap_fetch, light_llm_call, inbox_create, mail_move
 */
(function () {
    // …Orchestrator logic, vance.tools.call(...), vance.process.spawn(...)…
})();

JsScriptArchitect is a Schema Architect bean like the others, but with three key differences:

  1. isRecipeOutput() returns false — VALIDATING skips YAML parse, engine: field check, and path persistence check.
  2. outputPathSegment() / outputExtension() return "scripts" / ".js" — PERSISTING writes under _vance/scripts/_slart/<runId>/<name>.js.
  3. directExecutionSpawn(state) returns a DirectExecutionSpawn(engineName="hactar", engineParams={scriptRef, language}) — EXECUTING bypasses the RecipeResolver and spawns Hactar directly with the persisted Script.

Validation: JsScriptArchitect.validateDraftShape delegates to HactarService.validate(...) — parse + JSDoc header + Tool Allowlist intersect (single owner from planning/script-architect-executor-split.md §5.6).

Mode CREATE/UPDATE for SCRIPT_JS (Engine param mode, default CREATE):

  • CREATE — Generate script from scratch from Goal + Manuals.
  • UPDATE — Caller provides existingScriptRef (+ optional failureReason). LoadingExistingPhase loads the body into state.existingScriptCode; JsScriptArchitect.appendProposingContext injects it as an “EXISTING SCRIPT” block into the user prompt. PERSISTING writes a new version in the _slart/<newRunId>/- bucket — no in-place edit of the original file (analogous to the EDIT guarantee in §8 for Recipes).

MAGRATHEA_WORKFLOW Output Form (Workflow State Machine, NOT Recipe YAML — no engine: field):

description: |
  <description>
version: "1"                  # optional
parameters:                   # optional — validated on workflow_start
  <key>: { type: string, required: true, default: <value> }
bounds:                       # optional — HARD Stop
  maxTotalCostUsd: <number>
  maxWallclockSeconds: <number>
  maxTaskSpawns: <number>
start: <state-name>           # MANDATORY — must exist in states
states:                       # MANDATORY — at least one State
  <state-name>:
    type: agent_task          # agent_task | tool_task | shell_task |
                              # script_task | gate_task | timer_task |
                              # condition_task | workflow_task | terminal
    recipe: <recipe-name>     # agent_task: must be a known Recipe
    params: { prompt: "...", schema: { ... } }
    on: { success: <state> }  # Outcome → Follow-up State (Exact-Match)
    catch: { technical_error: <state> }
    retry: { maxAttempts: 3, on: [technical_error, timeout], backoffSeconds: 30 }

MagratheaArchitect differs from the Recipe Architects:

  1. isRecipeOutput() returns false — VALIDATING skips YAML engine: field check, justifications resolve, and path persistence check; validateDraftShape is the only shape validation entry point.
  2. persistsAtFlatPath() returns true — PERSISTING writes to _vance/workflows/<draft-name>.yaml (flat, directly startable), not to the _slart-sandbox. An existing document there is overwritten (the Document version layer maintains history).
  3. wantsExecutionValidation() returns false and the Recipe sets planOnly: true — no EXECUTING/EXECUTION_VALIDATING.

Validation: MagratheaArchitect.validateDraftShape delegates to MagratheaWorkflowLoader.validateYaml(...) (the same parser that the runtime freezes into StartRecord at startup — checks start/states, transition targets, task types) and then checks that each agent_task.recipe resolves via the Project RecipeLoader (analogous to Vogon’s worker check). The appendProposingContext provides the Slart LLM with the Project Recipe list, so that agent_task Recipes are not hallucinated. Details on the workflow data model: specification/public/workflows.md.

BENJY_RECIPE Output Form (Outer-Recipe of the Benjy Orchestration Worker — Engine + Params, no promptPrefix):

description: ...
engine: benjy
params:
  doRecipe: <recipe-name>                       # Required — Ford-Doer, one spawn per item
  taskTypes: [info, coding, planning, analysis]   # Subset; Default: all four
  features:
    interpret:  { recipe: <light-llm-profile> }  # Required
    route:      { recipe: <light-llm-profile> }  # optional — false = mechanical fallback policy
    check:      { command: "<build/test-cmd>" }  # optional — mechanical verification (Coding items)
    evaluate:   { recipe: <light-llm-profile> }  # optional
    reflect:    { recipe: <light-llm-profile> }  # optional
    escalation: { recipe: <spawnable Recipe> }  # optional — stronger Worker for stuck items
  maxInitialItems: 5                              # Safety-net caps, see benjy-engine.md §4/§6
  workTarget: { kind: WORK }

The Architect deliberately creates only the Outer-Recipe: a Benjy configuration is a suite (Outer + Doer + 4 Controller Profiles), but Slart emits one artifact per run. References are validated (resolve via the Project RecipeLoader + Kind Check); missing sub-recipes drive re-PROPOSE with inventory hint — the same open point as with MARVIN_RECIPE (§11, recursive spawn). The bundled benjy-* profiles and benjy-do-* Doers cover the common case; shape validation delegates to BenjyFeatureConfig. fromParams (the fail-fast that the Engine itself runs on the first loop entry — no parallel validator schema, no drift).

Author-only like magrathea-workflow: the bundled benjy-architect recipe sets params.planOnly: true — the run ends after PERSISTING with DONE, the generated Recipe is then spawned as a separate step (DONE payload carries the path). A Benjy run is a long-running iterative Worker with its own Doer spawns and Controller calls; its cost profile has no place in an authoring run, and EXECUTION_VALIDATING’s file path heuristic does not fit a params-only Recipe. wantsExecutionValidation() and wantsPathPersistenceCheck() are defensively false (Zaphod/Magrathea precedent).

Output Form Additively Extensible. A new SchemaArchitect bean ⇒ new enum value in OutputSchemaType ⇒ new output form section here. ProposingPhase, ValidatingPhase, and PersistingPhase are NOT changed (the latter reads outputPathSegment, outputExtension, persistsAtFlatPath + artefactNoun from the Architect).

5. Lifecycle and Recovery

Engine.runTurn

Lane-triggered like all Engines. Pseudocode:

runTurnInner:
  state = loadState(process)

  // Terminal check FIRST (avoid spurious flickers).
  if state.status in {DONE, FAILED, ESCALATED}:
    closeProcess; return

  // Drain — InboxAnswer flips state in place.
  for msg in ctx.drainPending():
    if msg is InboxAnswer:
      handleInboxAnswer(state, msg)
        // Matches state.pendingInboxItemId; depending on
        // pendingInboxKind calls applyConfirmationAnswer or
        // applyEscalationAnswer.

  advanceOnePhase(state)
  persistState(state)

  // Park check: Inbox dialogue ongoing?
  if state.pendingInboxItemId != null:
    process.status := BLOCKED
    return

  if state.status in {DONE, FAILED, ESCALATED}:
    closeProcess
    return

  process.status := IDLE
  scheduleTurn

Recovery Mechanism

advanceOnePhase:
  // 1. Recovery consumption FIRST.
  if state.pendingRecovery != null:
    recoveryCount++
    if recoveryCount > maxRecoveries:
      switch escalationMode:
        case FAIL:     state.status := ESCALATED
        case ASK_USER: postEscalationInbox(); state.status := ESCALATING
      pendingRecovery := null
      return
    state.status := pendingRecovery.toPhase
    // pendingRecovery remains set for now — the target phase
    // reads the hint, then resets it to Acting.

  // 2. Phase dispatch.
  switch state.status:
    case READY:        → FRAMING
    case FRAMING:      framingPhase.execute() → CONFIRMING
    case CONFIRMING:   confirmingPhase.execute() → GATHERING (unless parked)
    case GATHERING:    gatheringPhase.execute() → CLASSIFYING
    case CLASSIFYING:  classifyingPhase.execute() → DECOMPOSING
    case DECOMPOSING:  decomposingPhase.execute() → BINDING
    case BINDING:      bindingPhase.execute()
                       if pendingRecovery: stay (rolled-back next turn)
                       else: → PROPOSING
    case PROPOSING:    proposingPhase.execute() → VALIDATING
    case VALIDATING:   validatingPhase.execute()
                       if pendingRecovery: stay
                       else: → PERSISTING
    case PERSISTING:   persistingPhase.execute()
                       if planOnly: → DONE
                       else:        → EXECUTING
    case EXECUTING:    executeChildIfNeeded() (idempotent spawn);
                       Process parks BLOCKED until ProcessEvent
                       of the Child arrives via drainPending.
                       Child-DONE     → EXECUTION_VALIDATING
                       Child-FAILED   → FAILED
    case EXECUTION_VALIDATING:
                       executionValidatingPhase.execute()
                       + ContentValidatingPhase (if User Criteria)
                       if pendingRecovery: → PROPOSING (Recovery Loop)
                       else: → DONE
    case ESCALATING:   no-op (parked, drainPending wakes us)

  // 3. Safety-net: clear stale recovery if phase didn't.
  if pendingRecovery == consumedRecovery:
    pendingRecovery := null

Re-entry after Recovery: the target phase (DECOMPOSING or PROPOSING) reads state.pendingRecovery.hint as the first step and includes it in the next LLM prompt — then it clears pendingRecovery. Engine’s safety-net clear is only relevant for stub/no-op phases.

6. Engine Parameters (Control)

Recipe Author or Spawn Caller sets these on engineParams:

Param Values Default Effect
userDescription string (fallback to process.goal) Free-text user task
outputSchemaType see §4 vogon-strategy Which Recipe form is generated
planOnly boolean false true ⇒ Engine stops after PERSISTING with DONE; false ⇒ Engine spawns the generated Recipe as a Child and validates its outputs (EXECUTING + EXECUTION_VALIDATING, see §2)
proposingHints string (empty) Free-text append to the PROPOSING system prompt — used by Kits/Wrapper Recipes to inject Recipe shape conventions without changing the Engine prompts
confirmationMode DROP_LOW_CONF | KEEP_ALL | ASK_LOW_CONF DROP_LOW_CONF How low-conf assumed criteria are handled — see §7
escalationMode FAIL | ASK_USER FAIL What happens on recovery budget exhaustion — see §7
confirmationThreshold double 0..1 0.85 Confidence threshold for “high-conf assumed”
maxSpeculativeRatio double 0..1 0.30 Max proportion of speculative Subgoals
maxRecoveries int 5 Total budget for BINDING+VALIDATING recoveries
auditLlmCalls boolean true Append LlmCallRecord per LLM call
mode CREATE | EDIT | UPDATE inferred Explicit mode selection. Default derivation: existingScriptRef set → UPDATE; targetRecipeName set → EDIT; else CREATE. EDIT is recipe-only (in-place overwrite in _user/); UPDATE writes to a new _slart/<runId>/ bucket
existingScriptRef string (empty) UPDATE-mode for SCRIPT_JS: Document path to the existing script. Required for UPDATE; LOADING_EXISTING reads the body and stashes it on state.existingScriptCode
failureReason string (empty) UPDATE-mode optional: Hactar-TerminationRationale.failureReason from a prior FAILED run. Surface in the PROPOSING prompt as “what went wrong last time” context. Internally mapped to state.priorFailureReason
targetRecipeName string (empty) EDIT-mode: existing recipe to patch in _vance/recipes/_user/<name>.yaml. FRAMING-LLM can also extract from user description

Values are case-insensitive and tolerant of dash↔underscore. Unknown values → Default with WARN log.

7. Inbox Dialogue (M6.2)

Two places can make the Engine wait for a user response:

CONFIRMATION (mode=ASK_LOW_CONF)

Trigger: At least one assumed Criterion has confidence < confirmationThreshold.

Mechanism:

  1. ConfirmingPhase posts a MaximegalonType.APPROVAL with:
    • title: “Slartibartfast: Confirm N assumption(s)?”
    • body: List of low-conf Criteria with text + confidence
    • payload.kind = "slartibartfast.confirmation", payload.criteria = [{id, text, origin, confidence, rationaleId}]
  2. Phase sets pendingInboxItemId + pendingInboxKind=CONFIRMATION, returns without setting acceptanceCriteria.
  3. Engine sees pendingInboxItemId, sets process status BLOCKED.
  4. User responds via Inbox: {approved: true|false}.
  5. Engine.drainPending matches inboxItemId, calls applyConfirmationAnswer:
    • approved=true: each low-conf assumed Criterion gets origin → USER_CONFIRMED (then passes through).
    • approved=false: no update (passive abort — the originals remain low-conf INFERRED_* and are dropped in the next ConfirmingPhase round).
  6. ConfirmingPhase runs again, sees pendingInboxItemId=null, no more low-conf (either USER_CONFIRMED or same) → normal partition → status → GATHERING.

ESCALATION (mode=ASK_USER)

Trigger: recoveryCount > maxRecoveries.

Mechanism:

  1. Engine recovery handler posts MaximegalonType.APPROVAL with:
    • title: “Slartibartfast: Recovery budget exhausted — retry?”
    • body: Last Recovery Reason + Hint
    • payload.kind = "slartibartfast.escalation", payload.validationReport, payload.recipeDraft.
  2. Engine sets pendingInboxItemId + pendingInboxKind=ESCALATION, status ESCALATING.
  3. Process status → BLOCKED.
  4. User responds {approved: true|false}.
  5. drainPending → applyEscalationAnswer:
    • approved=true: recoveryCount=0, status → PROPOSING (fresh attempt).
    • approved=false: status → ESCALATED (terminal close).

Response Granularity (v1)

Both dialogues are binary (yes/no for the entire batch). Per- Criterion decisions would be possible via MaximegalonType.STRUCTURE_EDIT — if practice shows that the binary answer is too coarse.

8. Storage Convention for Generated Artifacts

Slartibartfast persists Recipes as Documents under the standard path recipes/<name>.yaml. To prevent generated Recipes from colliding with hand-authored or Kit-installed Recipes, each Slartibartfast spawn runs in its own Run-Bucket:

recipes/_slart/<runId>/<recipe-name>.yaml
recipes/_slart/<runId>/audit.json
Component Meaning
_slart/ Namespace prefix; matches the convention of _tenant for system projects
<runId> 8-hex char prefix of a UUIDv4, assigned once at spawn (architectState.runId)
<recipe-name> Name from RecipeDraft.name (LLM-generated, kebab-case)
audit.json Pretty-printed Jackson dump of the complete ArchitectState — audit + reproducibility

The Recipe is directly spawnable with process_spawn(engine="vogon"|"marvin", recipe="_slart/<runId>/<recipe-name>")RecipeLoader reads subdirectories under recipes/ transparently.

PERSISTING is idempotent (find-or-update). Audit write failure is non-fatal — if the Recipe is written, the run is considered successful.

Guarantee of Bucket Separation. As long as Slartibartfast writes exclusively to recipes/_slart/<runId>/, its outputs and Kit-installed Recipes (recipes/<name>.yaml at the same level) cannot physically overwrite each other. This guarantee is the basis for the two write paths into the Project configuration (deterministic Kit import, LLM-driven Slart run) to coexist conflict-free today. Any future extension that would allow Slartibartfast to patch existing Recipes outside the _slart/ bucket (Edit mode) must explicitly renegotiate this guarantee — otherwise, a kit update silently overwrites a Slart patch or vice versa.

9. DONE Payload (Contract)

When the run reaches DONE, the Engine emits a ProcessEvent with the ArchitectState as payload. Key fields for the caller:

Field Content
runId 8-hex bucket id
outputSchemaType see §4
persistedRecipePath recipes/_slart/<runId>/<name>.yaml
proposedRecipe.name Recipe name (Resolver form: _slart/<runId>/<name>)
proposedRecipe.confidence 0..1
childExecutionProcessId (only if planOnly=false) ID of the Child Execution Process that executed the generated Recipe
childExecutionOutcome (only if planOnly=false) DONE | FAILED | STOPPED of the Child run
terminationRationale.statedCriteriaSatisfied IDs of addressed stated Criteria
terminationRationale.assumedCriteriaTakenForGranted high-conf inferred assumptions
terminationRationale.assumedCriteriaUserConfirmed confirmed via Inbox (M6.2)
terminationRationale.evidenceCoverage 1.0 - speculative-ratio
terminationRationale.iterationCount how many PhaseIterations
terminationRationale.recoveryEvents how many recoveries passed through
terminationRationale.finalConfidence RecipeDraft.confidence

If planOnly=true, the caller (typically Arthur) reads the payload and either:

  • direct spawn: process_spawn(recipe="_slart/<runId>/<name>")
  • user approval: shows Recipe + TerminationRationale in chat, asks for confirmation, then spawns.

If planOnly=false, the Recipe has already been executed — the caller shows the result (childExecutionOutcome plus the output documents checked in EXECUTION_VALIDATING) and typically has nothing more to spawn.

10. Relationship to Trillian

Trillian (not yet implemented) would be the open variant — Goal without a fixed endpoint, iterative skeleton building with reflection. When to use which?

Aspect Slartibartfast Trillian
Input Description + Schema Type Goal (free text)
Setup Phased Workflow (10 Stages) Goal Internalization
Output Persisted Recipe YAML Live Run with Skeleton+Detail
Adaptive No (finished after DONE) Yes (skeleton can mutate mid-run)
Latency Low (~30-90s typical, 6-8 LLM calls) High (Reflection per step)
Determinism High Medium
When Task fits into Recipe form Truly open, long-running task

They are not mutually exclusive: Trillian could internally call Slartibartfast for skeleton generation, and otherwise generate freely.

11. Open Points

  • Self-execute loop for all schemas is implemented. EXECUTING + EXECUTION_VALIDATING (+ ContentValidatingPhase + Recovery loop back to PROPOSING, see §2 + §5) is schema-agnostic and applies to Vogon, Marvin, and Zaphod.
  • Sub-recipe generation for MARVIN_RECIPE output: today MarvinArchitect.validateDraftShape checks via RecipeLoader that each name in allowedSubTaskRecipes / recipesOnlyViaExpand is an existing Project Recipe; missing names cause VALIDATING to fail and drive re-PROPOSE with a concrete Recipe inventory hint. What remains open: if the LLM truly needs a new sub-recipe, today the user must install an extended Kit — a recursive Slartibartfast spawn per missing sub-recipe could automate this. The same applies to BENJY_RECIPE: the BenjyArchitect only creates the Outer-Recipe and references Doer + Controller Profiles — missing sub-recipes are the same open point, not a new one.
  • Per-Criterion Decisions in the Inbox dialogue (extension of M6.2): Instead of binary for the batch — MaximegalonType.STRUCTURE_EDIT with a boolean per Criterion. Awaiting concrete UX feedback.
  • Cost Caps (maxLlmCallsPerSpawn): Currently unlimited. With 6-15 calls per run (FRAMING + N×CLASSIFYING + DECOMPOSING + PROPOSING + Recoveries) ~$0.005-0.02 per run — acceptable, but a hard cap for runaway recovery loops would be useful.
  • Constraint Recursion (informational): If Slartibartfast itself spawns a Marvin Recipe via marvin-recipe output, its own controlling Recipe is also a Marvin Recipe. This is the Level 3 recursion from instructions/engines.md §”Level three”. Mechanism unchanged — each layer is a normal run.
  • Edit mode for existing Recipes (Future): “In Recipe X, replace Persona Head Y with Z” — architecturally excluded today because PERSISTING writes exclusively to recipes/_slart/<runId>/ (see §8 Guarantee). If implemented, bucket separation against kit update must be renegotiated.

12. Implementation Status

Status: Implemented (M0–M6). All phases real, all Engine parameters controllable, Inbox dialogue functional.

Milestone What Tests
M0 Data Model (DTOs, Enums) Roundtrip test green
M1 Engine Skeleton with Stub Phases Lifecycle test green
M2 FRAMING (real LLM) + ai-test
M3.1 CONFIRMING (pure logic) unit
M3.2 GATHERING (DocumentService) unit
M3.3 CLASSIFYING (real LLM) unit
M4.1 DECOMPOSING + BINDING + Recovery unit
M4.2 PROPOSING + VALIDATING unit
M4.3 PERSISTING + TerminationRationale unit + ai-test (full pipeline)
M5 MARVIN_RECIPE Output — production via MarvinArchitect (System Prompt + 4 Shape Validators incl. allowedSubTaskRecipes resolve). unit + ai-test (SlartibartfastMarvinRecipeLlmTest)
EX EXECUTING + EXECUTION_VALIDATING + ContentValidatingPhase unit + ai-test (FullPipeline)
AR Schema-Architects-Refactor — SchemaArchitect interface + VogonArchitect / MarvinArchitect / ZaphodArchitect beans; ProposingPhase + ValidatingPhase schema-agnostic. Plus ZAPHOD_RECIPE as third output schema production-ready. unit (ZaphodHeadsParserTest) + ai-test (ZaphodArchitectRecipeShapeLlmTest)
M6.1 confirmationMode + escalationMode (DROP/KEEP/FAIL) unit
M6.2 ASK_LOW_CONF + ASK_USER (Inbox Dialogue) (Test gap; see §11)
MW MAGRATHEA_WORKFLOW Output — author-only MagratheaArchitect (MagratheaWorkflowLoader.validateYaml + agent_task.recipe check, persistsAtFlatPath to _vance/workflows/<name>.yaml, planOnly). SPI extended by persistsAtFlatPath() + artefactNoun(). unit (MagratheaArchitectTest)
BA BENJY_RECIPE Output — author-only BenjyArchitect (Shape delegation to BenjyFeatureConfig.fromParams + reference resolve/kind check, bundled benjy-architect wrapper recipe with planOnly). Also the bundled Benjy variants benjy-research (Research-Doer), benjy-batch (route-from-cheap-mode) and benjy-do-research. unit (BenjyArchitectTest + extended BenjyRecipeConsistencyTest) + ai-test (BenjyArchitectRecipeShapeLlmTest — kit-less, evidence from bundled SHAPE manual; recovery loop empirically: VALIDATING-Recovery → Re-PROPOSE → PASS)

Prerequisites — all met:

  • Phase F (Vogon-Inline-strategyPlanYaml) — Slartibartfast emits inline params.strategyPlanYaml for VOGON_PLAN.
  • §2.5/§2.6 Vogon-Branch-Actions — internal Decider/JSON output patterns as templates for the phase system prompts.
  • Phase M/L/O/Q (Marvin-Constraint-Params) — the configuration knobs that Slartibartfast sets for MARVIN_RECIPE.
  • Phase N (Marvin-Sequencing) + Phase P (idempotent postActions) — so that a generated Marvin Recipe runs end-to-end.

Empirically verified (M5+M4.3 ai-tests):

  • VOGON_PLAN output with completely parser-valid Vogon Recipe incl. inline strategyPlanYaml (4-6 phases)
  • MARVIN_RECIPE output with engine: marvin, params (automatic defaultExecutionMode + disallowedTaskKinds + allowedExpandDocumentRefPaths matching Manuals), structured promptPrefix
  • Recovery loop engages live (VALIDATING #1 → PROPOSING #2 → VALIDATING #2)
  • 0 hallucinations in the tested runs against the essay-slart Kit