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. This is precisely 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 or too small, a Recovery-Loop is triggered
back to PROPOSING. 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, deterministic execution | “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
logs 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_STRATEGY: generates Recipe-YAML with
engine: vogon + inline strategyPlanYaml
- MARVIN_RECIPE: generates Recipe-YAML with
engine: marvin + params + promptPrefix
Provides 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_STRATEGY: 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 direct 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_STRATEGY | MARVIN_RECIPE | ZAPHOD_RECIPE | SCRIPT_JS | MAGRATHEA_WORKFLOW
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.
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 | VogonArchitect — StrategyResolver.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 | ZaphodArchitect — ZaphodHeadsParser.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) |
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 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, thus 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 only 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 checks 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_STRATEGY 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 Architect’s appendProposingContext
provides the Slart LLM with the Project Recipe list,
so 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:
isRecipeOutput()returnsfalse— VALIDATING skips YAML-Parse,engine:-Field-Check and path-persistence-Check.outputPathSegment()/outputExtension()return"scripts"/".js"— PERSISTING writes to_vance/scripts/_slart/<runId>/<name>.js.directExecutionSpawn(state)returns aDirectExecutionSpawn(engineName="hactar", engineParams={scriptRef, language})— EXECUTING bypasses theRecipeResolverand 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— Create script from scratch from Goal + Manuals.UPDATE— Caller providesexistingScriptRef(+ optionalfailureReason).LoadingExistingPhaseloads the Body intostate.existingScriptCode;JsScriptArchitect.appendProposingContextinjects 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 → Next State (Exact-Match)
catch: { technical_error: <state> }
retry: { maxAttempts: 3, on: [technical_error, timeout], backoffSeconds: 30 }
MagratheaArchitect differs from the Recipe Architects:
isRecipeOutput()returnsfalse— VALIDATING skips YAML-engine:-Field-Check, justifications-resolve and path-persistence-Check;validateDraftShapeis the only shape validation entry point.persistsAtFlatPath()returnstrue— PERSISTING writes to_vance/workflows/<draft-name>.yaml(flat, directly startable), not in the_slart-sandbox. An existing document there is overwritten (the Document version layer maintains history).wantsExecutionValidation()returnsfalseand the Recipe setsplanOnly: 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 agent_task-Recipes are not hallucinated.
Details on the workflow data model: specification/public/workflows.md.
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, 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 puts it
into 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 when Recovery budget is exhausted — 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; otherwise 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. Mandatory 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:
- ConfirmingPhase posts an
InboxItemType.APPROVALwith:title: “Slartibartfast: Confirm N assumption(s)?”body: List of low-conf Criteria with text + confidencepayload.kind = "slartibartfast.confirmation",payload.criteria = [{id, text, origin, confidence, rationaleId}]
- Phase sets
pendingInboxItemId+pendingInboxKind=CONFIRMATION, returns without settingacceptanceCriteria. - Engine sees pendingInboxItemId, sets process status
BLOCKED. - User responds via Inbox:
{approved: true|false}. - Engine.drainPending matches inboxItemId, calls
applyConfirmationAnswer:approved=true: each low-conf assumed Criterion getsorigin → 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).
- 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:
- Engine recovery-handler posts
InboxItemType.APPROVALwith:title: “Slartibartfast: Recovery budget exhausted — retry?”body: Last Recovery Reason + Hintpayload.kind = "slartibartfast.escalation",payload.validationReport,payload.recipeDraft.
- Engine sets
pendingInboxItemId+pendingInboxKind=ESCALATION, statusESCALATING. - Process status → BLOCKED.
- User responds
{approved: true|false}. - 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 InboxItemType.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_create(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 without conflict 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_create(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 generally 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.validateDraftShapechecks viaRecipeLoaderthat each name inallowedSubTaskRecipes/recipesOnlyViaExpandis 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 deploy an extended Kit — a recursive Slartibartfast spawn per missing Sub-Recipe could automate this. - Per-Criterion Decisions in the Inbox dialogue (extension of M6.2):
Instead of binary for the batch —
InboxItemType.STRUCTURE_EDITwith a Bool 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 frominstructions/engines.md§”Level three”. Mechanism unchanged — each layer is a normal run. - Edit mode for existing Recipes (Future): “Replace Persona-Head Y with Z
in Recipe X” — today architecturally
excluded because PERSISTING writes exclusively to
recipes/_slart/<runId>/(see §8 Guarantee). If implemented, the bucket separation againstkit updatemust 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) |
Prerequisites — all met:
- Phase F (Vogon-Inline-strategyPlanYaml) — Slartibartfast emits
inline
params.strategyPlanYamlfor VOGON_STRATEGY. - §2.5/§2.6 Vogon-Branch-Actions — internal Decider/JSON-Output- Patterns as template for the Phase-System-Prompts.
- Phase M/L/O/Q (Marvin-Constraint-Params) — the configuration buttons 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_STRATEGY-Output with completely parser-valid Vogon-Recipe incl. inline strategyPlanYaml (4-6 phases)
- MARVIN_RECIPE-Output with engine: marvin, params (auto. 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