ConceptsInterview → Manifest → Render

Interview → Manifest → Render

The heart of ai-core-kit is a small, frozen pipeline: an archetype-first interview writes a project.manifest.yaml, which a renderer consumes to stamp out your project. Everything here is the P3 contract — the stable interface several downstream consumers build against.

One producer, three read-only consumers. templates/interview/questions.yaml is the deterministic question bank; the /ack-init interview is the sole writer of managed: (machine-owned), walking the bank in order with no LLM and validating before render. Render (P4) substitutes ${VAR}, the gate (P5) reads contract_gate.* + contracts[], and telemetry (P6) buckets OFFLINE cost via telemetry.* — all read-only, so the single source of truth never drifts mid-run.
questions.yaml  ──(interview, no LLM)──▶  project.manifest.yaml  ──(validate)──▶  RENDER
 (the question bank)                       (single source of truth)               (P4)

One producer, many consumers

PRODUCER
  /ack-init interview     → the ONLY writer of the manifest
      • writes managed: wholesale from templates/interview/questions.yaml
      • seeds user: once on first run, then never touches it
      • validates against the JSON-Schema BEFORE any render

CONSUMERS (read-only; none of them writes the manifest)
  render engine           → substitutes ${VAR} into the archetype scaffold
  contract gate hook      → enforces contract_gate.* + contracts[]
  telemetry aggregator    → buckets OFFLINE cost via telemetry.*

The single-writer rule is what makes the manifest a trustworthy single source of truth: no consumer ever writes it back, so it cannot drift mid-run.

The four frozen artifacts

ArtifactFileRole
Manifest schema (human)templates/manifest/project.manifest.schema.yamlAuthoritative, annotated contract (schema_version: 3); doubles as a worked instance.
Manifest schema (machine)templates/manifest/project.manifest.schema.jsonJSON-Schema draft 2020-12 — the validator the interview runs.
Question banktemplates/interview/questions.yamlThe deterministic questions the interview walks in order.
Render contractdocs/RENDER-ENGINE.md${VAR} syntax, _when.* conditional dirs, render.map.yaml, idempotency.

The question bank is deterministic

/ack-init walks questions.yaml in order, with no LLM in the loop: the same answers in always produce a byte-identical manifest out (invariant I2). Each question declares:

FieldMeaning
idunique, stable, kebab-case; referenced by ask_if/skip_if.
promptthe human-facing question text.
typeselect | multiselect | text | bool.
optionsfor select/multiselect: list of {value, label?}.
defaultpre-filled value; for select it must be one of options.value.
applies_tothe archetypes this question is asked for, or all.
ask_if / skip_ifoptional gating expression over earlier answers.
writes_todotted path under managed: where the answer is written.

Archetype is the first, mandatory question (the branch axis)

The very first question is archetype (invariant I3). Its answer narrows every subsequent question through applies_to gating — evaluated before ask_if/skip_if. This is how, for example, persistence (DB/ORM/migration) questions fire only for the deep archetypes, and the design-system question fires only for fullstack and saas.

The bank ships six archetypes. Three are deepbackend-api, fullstack, and saas (the opinionated Vercel + Next.js App Router + shadcn/ui + Supabase + Clerk + Drizzle + Stripe stack added in schema v3) — and three are minimal core: monorepo, library-sdk, and infra-iac. The minimal-core archetypes answer only the universal questions (identity, features, contract_gate.mode + protected_paths, telemetry.enabled, ci_cd); the DB and design questions never fire for them — a deterministic outcome of applies_to, not an LLM judgment call.

Infrastructure-as-code is orthogonal to the archetype axis: the feat_iac toggle and the iac_provider/iac_tool questions let any archetype add an iac block (with derived is_aws/is_gcp booleans), so IaC is a feature, not a seventh branch.

The tiny expression grammar

ask_if/skip_if use an intentionally small, deterministic grammar — operators ==, !=, in, not_in, no parentheses or boolean chains — and may reference only earlier questions. A referenced id that was itself skipped evaluates to an unknown sentinel, which makes ==/in false and !=/not_in true (fail-safe: skip rather than ask). Example: migrations_tool carries ask_if: "migrations_enabled == true", so it never fires when migrations are disabled.

The full bank — all 45 questions and the branch matrix — is enumerated in the reference docs; the contract above is the part you need to reason about the flow.

The manifest envelope

schema_version: 3          # const 3; the FORMAT major version
generator:                 # provenance only; OUTSIDE the hash (re-runs stay byte-stable)
  tool: ai-core-kit
  tool_version: "0.4.0"
  rendered_at: "..."       # the only clock value; informational
managed:                   # MACHINE-OWNED. Regenerated wholesale every run.
  manifest_hash: "sha256:..."   # written LAST, over the canonical managed: subtree
  project: { name, language, ... }
  archetype: backend-api
  features: { hooks, mcp, agent_teams, sdd_gate, iac }  # iac is the v3 orthogonal toggle
  persistence: { ... }     # db now includes supabase (v3)
  auth: { provider }       # v3; saas defaults clerk
  hosting: { target }      # v3; saas defaults vercel
  billing: { provider }    # v3; saas defaults stripe
  iac: { provider, tool, is_aws, is_gcp }   # v3; present only when features.iac
  contract_gate: { mode, protected_paths, scope, exempt }
  contracts: [ ... ]
  telemetry: { ... }
  ci_cd: { target }
  rendered_files: [ ... ]   # ownership ledger; written by the renderer
user:                       # HUMAN-OWNED. Seeded once, then never overwritten.
  notes: ""
  overrides: {}

Schema v3 is the current FORMAT major. It adds the saas archetype, makes design_system required for both fullstack and saas, introduces the auth, hosting, and billing objects, adds supabase to the persistence.db enum, and adds the orthogonal features.iac toggle plus the iac block (whose is_aws/is_gcp are derived, not asked). Every new key defaults to “off”, so a v2 → v3 migration is render-neutral — but it is one-way and opt-in: /ack-init --migrate rewrites managed: to the v3 shape and recomputes the hash, and a v2 child is refused (not auto-upgraded) without it. Consumers refuse a mismatched major: the gate degrades to off + stderr, the aggregator errors.

The split is the whole idempotency story: managed: is machine-owned and regenerated every run; user: is human-owned and seeded exactly once. No consumer reads user: behaviorally — it is the user’s scratch space (the renderer may consult user.overrides.* for render vars but never requires it).

The global invariants

These are the frozen guarantees the whole pipeline rests on:

  • I1 — writes_to cross-check. Every question’s writes_to must resolve to a real property declared in the schema. /ack-init cross-checks this at startup and hard-refuses on any orphan key. The interview and the manifest can never silently diverge.
  • I2 — structural idempotency. managed:/user: split plus a canonical manifest_hash plus the rendered_files[] ledger give byte-identical output on identical answers. There is no 3-way merge engine.
  • I3 — archetype branching. archetype is the first mandatory question and narrows all subsequent questions.
  • I4 — the gate is never vacuous. contract_gate.protected_paths is required and non-empty per archetype, so even infra-iac (no src/**) can never ship an empty gate.
  • I5 — additionalProperties: false. A typo’d key is a hard validation error, not a silent drop.
  • I6 — fail-closed at author time, fail-open at runtime. Invalid manifest at write time aborts /ack-init; corrupt/missing manifest at consumption time degrades the gate to off plus stderr, never wedging a session.
  • I7 — forkability. The META discovery engine and .claude/ tooling are never copied into a child; rendered child paths use ${CLAUDE_PROJECT_DIR} and ${dotted.path}.

Validation gates the render

The order is load-bearing: answers → write manifest → validate → render. The renderer may assume a schema-valid managed: subtree because an invalid manifest aborts before any file is touched (author-time fail-closed). What happens next is the Render engine contract.

See also: Render engine contract · The META vs CHILD boundary · the full question reference and branch matrix in the reference docs.