nextv2.0.0
Run the server

Roll back to 1.4

What a rollback does to the database and the config directory, and the steps that take an upgraded install from 2.0 back to 1.4.

You will be on Kraft 1.4 again, with the database as 1.4 left it, and kraft --version prints kraft 1.4.0. A rollback needs that database, so take a backup before you upgrade. Step 1 checks that you have one, before it changes anything.

Before you start

  • You have the database as 1.4 left it, schema 44: a backup you took before the upgrade (a kraft-backup-<date>.db file in your home folder), or the copy Kraft made when you first left 1.4. That copy is not always the newest: a later upgrade adds a newer one that holds the schema before it, which 1.4 refuses. Step 1 finds the right one, and only reads.
  • When you upgraded to a 1.5.0 release candidate before 1.5.0rc12, Kraft made no copy, and only a backup you took yourself rolls back.
  • You know the Python Kraft runs on, and whether you use vector search. Step 3 installs 1.4 with both.
  • The sqlite3 command, for the schema checks in steps 1 and 4. Step 1 has a Python form for a machine without it.
  • You accept that the restore loses everything done on 2.0 since that backup: items filed, runs, approvals and their history.
  • If Kraft runs as a service, you know how to stop a service.

Roll back from 2.0 to 1.4

Step 1 only checks, so if it finds no database you stop there and stay on 2.0.

Steps 2 to 4 put back the release and the database. Steps 5 to 12 undo what 2.0 changed in the config directory, one file at a time: What a rollback does to $KRAFT_HOME lists them in one table. Step 13 starts 1.4. A step you skip, or get wrong, shows up as a symptom: If 1.4 will not start or misbehaves lists them by what you see and names the step that fixes each.

The commands that name your Kraft home set home or run themselves, from $KRAFT_HOME or ~/.kraft (run from $KRAFT_RUN_DIR first, as Kraft does), so each block works in a new terminal.

  1. Check that you have a database 1.4 can read, before you stop or install anything. Pick the database to restore: your own backup, or the copy Kraft made when you first left 1.4. Both must hold schema 44.
    To find Kraft's copy, follow Find the copy to restore: it is the newest one that prints 44. For your own backup, set backup to it; the command prints its schema, and a backup taken after the upgrade does not print 44. Put the path of your backup in the first line, in place of the example:
    backup="$HOME/kraft-backup-20261002-093015.db"
    sqlite3 -readonly "$backup" 'PRAGMA user_version'
    
    44
    

    Without the sqlite3 command, Python prints the same:
    backup="$HOME/kraft-backup-20261002-093015.db"
    python3 -c "import sqlite3, sys; print(sqlite3.connect('file:' + sys.argv[1] + '?mode=ro', uri=True).execute('PRAGMA user_version').fetchone()[0])" "$backup"
    

    Write down the path of the file you pick: step 4 uses it.
    If no copy prints 44 and you have no backup of your own, stop here. Nothing has changed, and you stay on 2.0. Which releases made no copy is in Before you start.
  2. Stop Kraft with kraft admin stop.
    If Kraft runs as a service, stop it as Stop a service says instead: the service manager starts a stopped server again, and the next steps must not run under a live one.
    Go on only when kraft admin stop prints kraft: stopped (pid N) or kraft: no server running. If it prints kraft: pid N did not stop within 15s and exits 1, the server is still running: do not touch the database.
    kraft: stopped (pid 48213)
    
  3. Install 1.4 with the Python you run Kraft on and the extra you have. On Homebrew, which cannot install an older release, skip the command below and see Pin a version. Leave out [vector] if you don't use vector search:
    uv tool install --force --python 3.X "kraft-sdlc[vector]==1.4.0"
    

    If you skipped step 2 and install an older version while the newer server still runs, the web UI says An older Kraft is installed and points here, rather than telling you to restart.
    Check the install:
    kraft --version
    
    kraft 1.4.0
    

    If it prints another version, 1.4 is not what your shell runs. Read the error the install printed, and run command -v kraft to see which kraft comes first on PATH. Repeat this step before you go on.
  4. Restore the database from step 1. Follow steps 2 and 3 of Restore a backup, in that order, with backup set to the file from step 1. Each block sets its own variables, so they work in one terminal or two. Skip step 1 there, because step 2 here stopped Kraft. Skip its steps 4 and 5 too: steps 5 to 12 here handle the config directory, and step 13 starts Kraft.
    Check the restored database:
    run="${KRAFT_RUN_DIR:-${KRAFT_HOME:-$HOME/.kraft}/run}"
    sqlite3 -readonly "$run/orchestrator.db" 'PRAGMA user_version'
    
    44
    

    Without the sqlite3 command, Python prints the same:
    run="${KRAFT_RUN_DIR:-${KRAFT_HOME:-$HOME/.kraft}/run}"
    python3 -c "import sqlite3, sys; print(sqlite3.connect('file:' + sys.argv[1] + '?mode=ro', uri=True).execute('PRAGMA user_version').fetchone()[0])" "$run/orchestrator.db"
    

    If it prints another number, the restore put in a copy 1.4 refuses: repeat steps 1 and 4 with a copy that prints 44. If it prints an error, no database is in place: read the message the restore block printed, and run it again. A restore from the wrong copy also makes 1.4 refuse to start (database schema vN is newer than code vM in run/logs/server.log).
  5. Check the config directory. 1.4 reads templates under $KRAFT_HOME, which is ~/.kraft unless you set it. 2.0's first start renamed it to config and left templates as a link to it, which 1.4 follows. Run:
    home="${KRAFT_HOME:-$HOME/.kraft}"
    ls -ld "$home/templates"
    
    • If it prints a link (templates -> config), there is nothing to do: go to step 6.
    • If it prints No such file or directory, the link is gone: rename the directory back. When 1.4 finds no templates/, it seeds a fresh default home beside yours, with no repos, a default policy and no access.yaml. The block moves nothing if templates exists, because mv would put config/ inside it:
      home="${KRAFT_HOME:-$HOME/.kraft}"
      if [ -e "$home/templates" ] || [ -L "$home/templates" ]; then
        echo "$home/templates exists: nothing moved"
      else
        mv "$home/config" "$home/templates"
      fi
      
    • If it prints a directory (drwx..., no arrow), not a link, 2.0 found a config/ you had made and could not merge the two (kraft admin doctor's config row named both). Move what templates/ still holds into config/, keeping the copy you want of each. Then set templates/ aside under a dated name and put a link in its place. The block moves nothing unless templates is a real folder, not a link, and config is a folder:
      home="${KRAFT_HOME:-$HOME/.kraft}"
      old="$home/templates.old-$(date +%Y%m%d-%H%M%S)"
      if [ -L "$home/templates" ] || [ ! -d "$home/templates" ]; then
        echo "$home/templates is not a real folder: nothing moved"
      elif [ ! -d "$home/config" ]; then
        echo "$home/config is not a folder: nothing moved"
      elif [ -e "$old" ]; then
        echo "$old is already there: run this again"
      else
        mv "$home/templates" "$old" && ln -s config "$home/templates" && ls -ld "$home/templates"
      fi
      

      The last command prints the new link, templates -> config. Nothing is deleted: templates.old-<time> holds whatever you did not move into config/. Delete that folder once 1.4 runs and Check it worked passes, and not before.
    • If you pointed KRAFT_CONFIG_DIR at another directory, 2.0 never renamed it. Point 1.4's KRAFT_TEMPLATES_DIR at it instead.
  6. Write each schedule's cron field the way 1.4 reads it, in the schedules: list of intake.yaml, before you move it. 1.4 reads only * or a comma list of numbers. Write a range or a step as that list (1-5 as 1,2,3,4,5, */15 as 0,15,30,45) and a day of week 7 as 0. If you skip this, see If 1.4 will not start or misbehaves.
  7. Move the schedules to policy.yaml. 1.4 reads them as triggers: in policy.yaml, not as schedules: in intake.yaml. The entries have the same shape, so cut the list from intake.yaml and paste it under a triggers: key in policy.yaml.
    Add the key yourself: 2.0 removed it once every trigger had moved, and the # triggers: in a seeded file is a commented example, so pasting under that line breaks the file. If some triggers stayed in policy.yaml, paste under the key that is there. If you skip this, see If 1.4 will not start or misbehaves.
  8. Delete the schedules: key itself from intake.yaml, even an empty schedules: [], because 1.4 refuses an intake.yaml that has one. If you skip this, auto-intake stays off: see If 1.4 will not start or misbehaves.
  9. Delete forge_poll_s from policy.yaml, if you set it, because 1.4 rejects the file with it. max_concurrent in policy.yaml needs nothing: 1.4 reads it there. If you skip this, see If 1.4 will not start or misbehaves.
  10. Rename the chain key in repos.yaml. 1.4 reads a repo's chain only as default_chain_template and skips default_chain. Rename each default_chain: to default_chain_template:.
    Where an entry has both keys (2.0 can save the new one beside the old), put default_chain's value, the one 2.0 used, in default_chain_template, then delete the default_chain line, so no entry ends with both. If you skip this, see If 1.4 will not start or misbehaves.
  11. Rewrite the agent profiles in harnesses.yaml. An agent profile saved on 2.0's Settings › Harnesses is written as providers:, which 1.4 refuses, and with it the whole file. Rewrite each such agent profile in 1.4's shape, one model: map by provider and one effort: for all of them:
    # 2.0
    strong:
      providers:
        claude: { model: sonnet, effort: high }
        codex: { model: gpt-5.6-terra, effort: high }
    # 1.4
    strong:
      model: { claude: sonnet, codex: gpt-5.6-terra }
      effort: high
    

    1.4 has one effort per agent profile: where the providers set different ones, pick one. An agent profile already in the model: shape needs nothing. If you skip this, see If 1.4 will not start or misbehaves.
  12. Restore the theme. 1.4's interface takes its colors from palette, which 2.0 removed, so without the copy it shows its default colors. Copy the file 2.0 kept over theme.yaml. If a 1.5.0 release candidate converted the theme first, use the candidate's file, named in Your theme, in place of theme.yaml.pre-2.0.
    The copy is your theme from the first upgrade past 1.4, and is never replaced. The block first moves the current theme.yaml aside as theme.yaml.before-rollback-<time>, so a theme you changed on 1.4 earlier is kept there, and copies over only when the source file exists. Put the candidate's file in copy if one converted the theme. Delete the dated file once 1.4 shows the theme you want, and not before:
    home="${KRAFT_HOME:-$HOME/.kraft}"
    copy="$home/templates/theme.yaml.pre-2.0"
    saved="$home/templates/theme.yaml.before-rollback-$(date +%Y%m%d-%H%M%S)"
    if [ ! -f "$copy" ]; then
      echo "$copy is not a file: nothing changed. Set copy to the right file, or edit theme.yaml by hand"
    elif [ -e "$saved" ]; then
      echo "$saved is already there: run this again"
    elif [ -e "$home/templates/theme.yaml" ] && ! mv "$home/templates/theme.yaml" "$saved"; then
      echo "could not move theme.yaml aside: nothing changed"
    else
      cp "$copy" "$home/templates/theme.yaml" && ls -l "$home/templates/theme.yaml" || echo "the copy failed: the theme you had is at $saved, if there was one"
    fi
    

    With no copy, open theme.yaml and look for group_by: chain, which 2.0 writes when the board is grouped by chain, and change it to group_by: template. Also look for an editor: line, which 2.0 writes when you pick a Default editor in Settings › Appearance, and delete it. Or delete the file. 1.4 refuses both, and its board then loads no theme: see If 1.4 will not start or misbehaves.
  13. Start Kraft with kraft admin start --detach, or with kraft admin install-service if it ran as a service. If 1.4 does not start, see If 1.4 will not start or misbehaves.

Check it worked

Run:

kraft --version
kraft 1.4.0

Then run kraft admin templates lint and kraft admin doctor.

You know it worked when --version prints kraft 1.4.0, the board loads, templates lint exits 0, and doctor shows no failing templates row (1.4's name for the config directory row) or keys row.

If one of them fails, or the board misbehaves, find what you see in If 1.4 will not start or misbehaves.

If 1.4 will not start or misbehaves

What you seeWhyWhat to do
database schema vN is newer than code vM in run/logs/server.logThe database is not the one 1.4 left: a newer Kraft migrated it.Restore a copy that prints 44: steps 1 and 4.
Every new item is refused. For forge_poll_s, the log says policy config invalid, refusing workpolicy.yaml has what 1.4 rejects: forge_poll_s, a cron range or step, or a triggers: pasted under the commented # triggers: line.Steps 6, 7 and 9.
Auto-intake stays off while kraft admin health says okintake.yaml still has a schedules: key.Step 8.
A cron with day of week 7 never fires1.4 loads 7 and never fires it.Write it as 0: step 6.
Doctor's keys row fails, and the repo files on defaultrepos.yaml has default_chain, which 1.4 skips.Step 10.
harnesses.yaml is refused as a wholeAn agent profile is written as providers:.Step 11.
The board shows default colors, or loads no themetheme.yaml has surface, accent and colour_amount, group_by: chain, or an editor: line.Step 12.
A fresh default home appears beside yours, with no repos1.4 found no templates/.Step 5.

What the restore leaves behind

Restoring the backup loses everything done on 2.0 since you took it: items filed, runs, approvals and their history. It does not remove what those runs made outside the database. Their worktrees under run/worktrees/, their branches, and any merge requests they opened are still there, and 1.4 knows nothing about them.

  • kraft admin doctor's worktrees row lists a worktree with no work item. Delete the ones you do not need, and close any merge request yourself. 1.4's row also lists the worktrees of items you cancelled on 1.4, which its board does not show.
  • A worktree made on 2.0 is one whose id is not in the database you restored. This lists the ids that are. -readonly makes a wrong path an error, not an empty database where Kraft is about to start:
    run="${KRAFT_RUN_DIR:-${KRAFT_HOME:-$HOME/.kraft}/run}"
    sqlite3 -readonly "$run/orchestrator.db" "select id from work_items"
    

    Without the sqlite3 command:
    run="${KRAFT_RUN_DIR:-${KRAFT_HOME:-$HOME/.kraft}/run}"
    python3 -c "import sqlite3, sys; [print(r[0]) for r in sqlite3.connect('file:' + sys.argv[1] + '?mode=ro', uri=True).execute('select id from work_items')]" "$run/orchestrator.db"
    

What a rollback does to $KRAFT_HOME

Starting a newer Kraft migrates run/orchestrator.db forward in place, and there is no downgrade migration. An older Kraft refuses to start on a database a newer one migrated (database schema vN is newer than code vM, in run/logs/server.log).

So back up run/orchestrator.db before you upgrade, and restore that backup when you roll back. If you have none, restore the copy Kraft made before migrating: see Find the copy to restore. run/index.db needs nothing: an older Kraft rebuilds a search index it cannot read.

A rollback does not undo what a newer version changed in the config directory. What 2.0 moved, you move back by hand, and kraft admin templates lint after a rollback checks the older version still accepts the chains. The steps above do it file by file:

In config/What 2.0 didWhat 1.4 needsStep
The directoryRenamed templates/ to config/, left a templates linktemplates/5
intake.yaml and policy.yamlSchedules moved to schedules: in intake.yaml; forge_poll_s may be set in policy.yamltriggers: in policy.yaml, cron as numbers, no schedules: key, no forge_poll_s6 to 9
repos.yamldefault_chaindefault_chain_template10
harnesses.yamlAgent profiles written as providers:A model: map and one effort:11
theme.yamlsurface, accent and colour_amount; group_by: chain when the board is grouped by chain; an editor: line when you pick a Default editorpalette, group_by: template and no editor: line12

Next steps

Copyright © 2026