Version. 0.1
Date. 2026-06-02
Provenance. Engine-source trace, Operator: Marvin Percival. Consolidates seed-creation material previously scattered across the seed-investigation reports into one map.
Status. Investigation / map of what exists today. Not a design and not a build. No mechanism change is proposed here. This is the artifact future seed-creation decisions are made against; it is measured by accuracy, not brevity.
Lens. Read alongside standing-notes/loomworks-standing-note-engine-wrapper-boundary-v0_1.md. §6 marks engine-vs-wrapper per that note, and flags where the boundary is already not clean.
Companion deep-dive. The three-terminal-paths divergence (§5) is analysed in depth — why each path exists, which fires for the live operator, and whether it is a problem — in investigations/loomworks-seed-creation-three-paths-deep-dive-v0_1.md.
Code citations. All engine: references are to the loomworks-engine repo at the time of writing. Line numbers are indicative; function/file names are load-bearing.
There is one shared engine core for committing a seed (draft_seed → induct_seed → commit_engagement → _create_engagement), and several front-ends that reach it. The structured six-field Seed is the engine's truth; it lives in the administrative engagement's event log and is pointed to by a candidate engagement row's candidate_seed_id. The form path authors the six fields directly. The conversational path runs a Companion conversation that produces a prose brief + a structured seed_fields blob on conversation-turn events only (no Seed object yet), and then a terminal step turns that conversation into a real Seed and inducts it. The boundary between "conversation, no Seed object" (Sub-phase A) and "Seed object exists, induction with findings" (Sub-phase B) is the first draft_seed call. Everything converges on commit_engagement.
Engine-core functions (engine: src/loomworks/engagement/creation.py):
draft_seed (creation.py:82)Seed memory object, version=1, object_type="seed", under ADMINISTRATIVE_ENGAGEMENT_ID (its only home pre-instantiation). Event kind seed_drafted.seed_requirements_engagement_version_at_induction to the admin engagement's current version (R-A12), via ensure_seed_requirements(db) (creation.py:103).candidate_seed_id).amend_candidate_seed (creation.py:145) (pre-instantiation refinement)Seed version — same id, version+1, provenance.wasRevisionOf → prior. Event kind seed_amended. Fields left None carry forward (_pick, creation.py:181).revise_seed: "Pre-instantiation seeds have no committed engagement yet, so the consideration framework doesn't apply" (creation.py:159-167).candidate_seed_id still resolves to the same UUID.induct_seed (creation.py:409) — one cycle per callagent.examine(...) through an AgentRunner (creation.py:448) — the seed-induction agent (engine: src/loomworks/engagement/seed_induction.py:68, SeedInductionAgent.examine at :93).persisting / genuinely_new / prior_addressed (creation.py:466-483); writes only genuinely_new as finding_produced events; prior-addressed findings are marked addressed (version+1, state="addressed").SeedInductionLoopCycle per call (creation.py:529).converged = (open_after_count == 0) (creation.py:501) — no "good enough" path.this_cycle_number >= max_cycles (default 10) and open count did not reduce (creation.py:510-520); returns a divergence_diagnosis, does not raise.InductionOutcome returned to caller.commit_engagement (creation.py:681) / commit_engagement_divergent (creation.py:725)kind != "agent", else AgentCannotCommitError, creation.py:702); seed must be converged (_load_induction_state → no open findings, else NotConvergedError, creation.py:708). The divergent variant instead requires a non-empty rationale (EmptyRationaleError, creation.py:749) and records is_divergent=True._create_engagement._create_engagement (creation.py:847) — the convergence pointderive_title_from_seed(what_the_work_is, operator_supplied_name=seed.additional_assertions.get("engagement_name")) (creation.py:860-863).engagement_id passed → INSERT INTO engagements (id, title); if passed (the HTTP front-ends) → promote in place: UPDATE engagements SET state='active', candidate_seed_id=NULL, title=COALESCE(NULLIF(title,''), :title) (creation.py:875-883).Engagement v1 with seed_ref=MemoryRef(seed.id, seed.version), is_divergent, divergence_rationale; event engagement_committed (creation.py:899).wasDerivedFrom from Engagement → founding seed version (creation.py:913-924).candidate_seed_id cleared; seed pinned via seed_ref.
(Post-commit amendment — out of creation scope but for completeness: revise_seed (creation.py:229) is the Phase 6 consideration-anchored amendment, paired with revise_engagement (creation.py:764) to advance seed_ref. That path is governance-bearing; it is not part of creation.)
The transition is the first draft_seed call. Before it: a conversation. After it: a structured Seed object plus induction.
Seed objectPOST /engagements/{eid}/seed/converse → converse_route (engine: src/loomworks/api/routers/seed_conversation.py:495).operator_turn event; the full transcript is replayed to the LLM (_build_messages_array, seed_conversation.py:340 — re-sends prior companion turns' raw XML output); the LLM emits <reply>/<seed_fields>/<brief>/<completion>/<status> (parsed at _parse_llm_response, :254); a companion_turn event is appended carrying seed_document (brief), seed_fields (structured), completion_percentage, status (_append_companion_turn, :445-483).converse_route never calls draft_seed or induct_seed. There is no Seed object, no candidate_seed_id seed link, no findings, no version. The structured fields are a per-turn LLM output stored on the turn event, kept "in sync" with the brief only because one LLM call co-produces both from the same transcript (the whole-transcript re-synthesis loop).seed_fields (on turns), completion, status. Does not exist in A: a Seed object, induction, findings, cycles, an Engagement.Seed first exists when a terminal step calls draft_seed (§4 of _execute_brief_path, the Phase 54 commit, the discovery extract, or the form's draft_engagement). That call is the A→B boundary.Seed object exists, induction with findingsSeed v1 is in the admin log; candidate_seed_id points at it; induct_seed runs producing Findings and SeedInductionLoopCycles.amend_candidate_seed (version bump) + re-induct, surfaced through the form's finding-cards-anchored-to-fields. There is no narrative-refinement round-trip operating against the inducted candidate seed — the whole-transcript narrative loop lives entirely in A, before the Seed and findings exist.
| Moment | Operator touch-point | Mechanism behind it |
|---|---|---|
| Start (conversational) | Types/speaks an idea; answers the Companion's questions | POST …/seed/converse turns (Sub-phase A) |
| Reads the brief | Sees prose "brief"; in the OL it's the read-only DraftSpecification on /chat | seed_document on each companion_turn; OL via draft_specification |
| Ready signal | Companion says "take a look — does this capture it?" | LLM sets status="ready_for_review" (prompt, creation_conversation.py:148-153) |
| Start (form) | Fills six plain-language fields; Submit for review | POST /engagements → draft_engagement (Sub-phase A skipped entirely) |
| Refine (form) | Edits fields; Submit amendment and re-induct; sees finding cards under fields | amend_candidate_seed_route + induct_engagement_seed |
| Commit ceremony | Commit this engagement → WebAuthn; or Commit despite findings → rationale modal | instantiate_challenge + instantiate_engagement / …/divergent |
| Title | (today) post-commit rename only | PUT /engagements/{eid}/title (metadata-only) |
The conversational operator never edits fields or the brief directly — input is always a conversational message (text or voice via …/seed/converse/transcribe).
Recorded on the candidate engagement id (Sub-phase A conversation) — engine: seed_conversation.py:
conversation_started (once), operator_turn, companion_turn (carries seed_fields/brief/completion/status/raw_llm_response), key_provided. Persisted events, but no Seed/Engagement governance — the transcript is durable; nothing is versioned-as-seed or governance-bearing yet.
Recorded under ADMINISTRATIVE_ENGAGEMENT_ID (the Seed's home) — engine: creation.py:
seed_drafted (Seed v1) — first exists at the terminal draft_seed call. Versioned, pre-instantiation, not governance-bearing (no Engagement, no consideration). Discardable.seed_amended (Seed v+1, wasRevisionOf) — versioned, still pre-instantiation, not governance-bearing.finding_produced (Findings), SeedInductionLoopCycle — induction history, walkable (R-A17).
On the engagements row (engine: DB):
draft_engagement (form) or _allocate_candidate_engagement (conversational), state='candidate', created_by_person_id set; candidate_seed_id wired after draft_seed. Promoted to state='active', candidate_seed_id=NULL at commit. title set/derived at commit.Recorded on the new engagement id at commit:
engagement_committed (Engagement v1, seed_ref); wasDerivedFrom relationship to founding seed.
Weight ladder: conversation turns (persisted, no seed/engagement weight) → candidate Seed versions (versioned, pre-instantiation, freely discardable, not governance-bearing) → committed Engagement + post-commit revise_seed (governance-bearing, consideration-anchored). The "edits are free pre-commit" property holds throughout A and B in the governance sense (no Engagement), but in B the Seed is versioned (not literally silent).
POST /engagements (draft_engagement, engagements.py:220) → POST …/seed (amend_candidate_seed_route, :301) → POST …/seed/induct (induct_engagement_seed, :347) → GET …/seed/findings (:429) → POST …/instantiate/challenge (:653) → POST …/instantiate (instantiate_engagement, :467, WebAuthn via get_committing_contributor + challenge store) → commit_engagement. Authors the six fields directly; enters at Sub-phase B immediately (no conversation).
Operator-layer POST /operator/converse (engine: src/loomworks/orchestration/routers/converse.py, operator_converse, registered as /converse at :1094) classifies intent (or honours intent_hint) and delegates:
create_project → _call_phase31_converse (converse.py:170) → converse_route with _internal_delegation=True (Sub-phase A).commit_project_draft → _call_seed_commit_from_brief (converse.py:196) → Phase 54 terminal.create_engagement_commit → _call_create_engagement_from_conversation (converse.py:306) → Phase 55 terminal.POST /engagements/{eid}/seed/converse/commit (engine: src/loomworks/api/routers/seed_commit_from_brief.py:224). Reads the LLM-emitted seed_fields off the latest ready_for_review companion_turn (_load_latest_ready_for_review_companion_turn, :159), adapts it (_adapt_seed_fields_for_draft_seed, :97 — maps voice→voice_of_the_work, list/dict additional_assertions, fills R-A10), calls draft_seed, wires candidate_seed_id, writes seed_committed_from_brief event, then induct_seed. Uses the structured representation the conversation produced.brief path — POST /me/engagements/create-from-conversation (engine: src/loomworks/api/routers/me_create_engagement.py:482) → _execute_brief_path (:345). It composes from raw turns: _compose_brief_from_turns builds only what_the_work_is and draft_seeds with empty defaults for the other five fields (me_create_engagement.py:374-385), then inline induct_seed. Writes terminal engagement_created_from_assistance on the personal engagement; has a Fault-B resume-and-commit guard (:574-604) so committing a reviewed candidate doesn't re-extract a degenerate transcript-dump seed.
Correction (verified — see the deep-dive): this path does NOT "ignore" structured seed_fields. Its conversation surface never produces them. Phase 55's create_engagement_active flow is project-less and does not delegate to Phase 31 (router.py:_route_create_engagement_active:1384 — it runs the general responder over Phase 42 relational ConverseTurn rows, which carry no seed_fields, under no Phase 31 structured-extraction XML contract). So compose-from-turns is a necessity of this surface's conversation store, not a discard of available structure. Contrast Phase 54, whose create_project flow always carries a project_id and delegates every turn to Phase 31 (_call_phase31_converse), which does write seed_fields onto companion_turn Memory events — which is why Phase 54 can reuse them.
discovery_record path — _execute_discovery_record_path (me_create_engagement.py:399): composes a Markdown discovery record from turns → Phase 16 upload → Phase 53 extract_seed_from_discovery (LLM extraction) → induct_seed.
All three (and the form path) converge on commit_engagement → _create_engagement. They diverge on how the structured Seed is first produced: form fields, vs. the LLM seed_fields (Phase 54), vs. thin compose-from-turns (Phase 55 brief), vs. discovery extraction (Phase 55 discovery). This is the single most scattered part of the mechanism — the same operator idea becomes a Seed in at least three different ways. The root cause is that the two conversational generations read from two different conversation stores: the Phase 31/Phase 54 lineage runs the structured-extraction engine over Memory-event companion_turns (which carry seed_fields), while the Phase 55 lineage runs the general responder over relational ConverseTurn rows (which do not). Phase 54 reuses structure because its surface produces it; Phase 55-brief composes from raw turns because its surface never does. Phase 55-discovery re-derives structure via a separate Phase 53 LLM extraction. The full why-each-exists / which-fires-today / is-it-a-problem analysis is in the companion deep-dive (see Lens).
Engine mechanism (must survive any wrapper):
draft_seed, amend_candidate_seed, induct_seed, commit_engagement / _divergent, _create_engagement, revise_seed / revise_engagement (creation.py).Seed / Finding / SeedInductionLoopCycle types and the R-A5–R-A11/R-A12/R-A16/R-A17 contract (engine: engagement/types.py).seed_ref; wasDerivedFrom.GET /engagements/{eid}/seed (engine: api/routers/seed_view.py) — the clean read interface a wrapper would use.Current-Companion-specific wrapper behavior:
engine: src/loomworks/prompts/creation_conversation.py) — the companion voice, the seven-question shape, "never say I'm an AI," the <seed_fields>/<brief> XML emission contract.ready_for_review framing./operator/converse classifier + route_intent + the translator flattening (engine: orchestration/translators.py:284 — maps seed_document → draft_specification, status → readiness, and drops seed_fields + completion_percentage).Boundary-not-clean flags (engine mechanism that currently ASSUMES the Companion):
api/routers/seed_conversation.py calls CREATION_CONVERSATION_PROMPT directly. The whole conversational-creation surface — a Companion artifact — sits inside the engine's API layer rather than in a wrapper talking through a boundary. A different wrapper inherits this prompt and its exact XML output contract.seed_fields off a companion_turn event in the Companion's emission shape. seed_commit_from_brief.py + _adapt_seed_fields_for_draft_seed exist specifically to translate the Phase 31 LLM's output (voice key, list/dict additional_assertions). The engine commit path is coupled to how this Companion emits structure._compose_brief_from_turns / _compose_markdown_discovery_record (me_create_engagement.py) consume ConverseTurn rows and the creation-mode-turn filter — engine terminal logic reaching into the Companion's conversation store and its turn semantics (incl. the Fault-B resume heuristic)._create_engagement derives the title from additional_assertions.engagement_name (creation.py:862) — an operator/Companion naming convention embedded in the engine's title derivation.draft_engagement materializes operator membership "so the creator can complete the conversational-path setup (key prompt → first turn)" (engagements.py:239-251) — the engine row-creation accommodating a Companion-flow detail.seed_fields + completion, and the operator-layer boundary discards them, so the OL can only ever see prose. Per the standing note this should be the wrapper choosing prose, exposed as a deliberate boundary offering both — today it is a fixed flattening.These six are the places where, if the Companion were replaced, engine code would have to change — i.e. where the boundary is not yet clean. They are recorded here as observations, not defects to fix in this artifact.
CONVERSATIONAL (Surface A) FORM (Surface B)
POST /operator/converse (wrapper: classify+route)
│ create_project / a turn POST /engagements
▼ (draft_engagement)
POST …/seed/converse ── Sub-phase A ──┐ │ allocate row + draft_seed + wire candidate_seed_id
operator_turn / companion_turn │ │
{seed_fields, brief, completion, │ ▼ ── enters Sub-phase B directly ──
status} on the turn event │ POST …/seed (amend_candidate_seed) ⇄ POST …/seed/induct
NO Seed object yet │ │ (loop: findings ↔ amend)
│ status→ready_for_review │ │
▼ │ │
┌─ commit_project_draft ──────────────┐│ │
│ POST …/seed/converse/commit (P54) ││ │
│ reads LLM seed_fields → draft_seed││ │
└─────────────────────────────────────┘│ │
┌─ create_engagement_commit ──────────┐│ │
│ POST /me/…/create-from-conversation ││ │
│ (P55) brief: compose-from-turns ││ │
│ OR discovery: extract ││ │
│ → draft_seed ││ │
└─────────────────────────────────────┘│ │
│ ===== A→B boundary: first draft_seed ===== │
▼ ▼
induct_seed (cycles, findings, convergence/divergence)
│ converged + human actor
▼
commit_engagement / _divergent → _create_engagement
(row → active, candidate_seed_id=NULL, Engagement v1, seed_ref, wasDerivedFrom)
▼
ENGAGEMENT ACTIVE
Investigation / map only. No engine or wrapper code changed. No mechanism redesigned or proposed. Filed in loomworks-record/investigations/. Pairs with standing-notes/loomworks-standing-note-engine-wrapper-boundary-v0_1.md (the lens), investigations/loomworks-seed-creation-three-paths-deep-dive-v0_1.md (the §5 deep-dive), investigations/loomworks-seed-creation-engine-contract-v0_1.md (the interface contract), investigations/loomworks-engine-boundary-inventory-v0_1.md (the whole-engine boundary), and investigations/loomworks-title-review-at-induction-design-v0_1.md.