6123d2cfa911

wip: prepare Koh 3.4.4 migration gate and release targets; release stripping fails, regenerate Linux artifacts

time
2026-10-03 22:26 UTC · 1 week ago
machine
Junes-MacBook-Pro.local
agent
gpt-6
session
release koh three
parent
4dc9a406ab30
11 files README.md+15 −7VERSION+1 −1build.sh+26 −0koh.build+4 −0shell/koh3.plugin.zsh+7 −3src/cli/cli.v+3 −1src/cli/handover.v+6 −61src/koh.v+9 −0src/koh3.v+0 −9tests/cli_test.v+11 −15v.mod+1 −1
README.md+15 −7
@@ -45,8 +45,8 @@4545 made them and in which session, so `koh undo --session` can take back an4646 agent's whole run in one step. A person has to be at the keyboard to `strip`.4747 48-**Small on purpose.** Koh 3 has 17 commands, down from Koh 2's 45, and a hard49-ceiling of 10,000 lines of V. A feature that doesn't fit gets cut, so one48+**Small on purpose.** Koh 3 has 20 commands plus the manual, down from Koh 2's 45, and a hard49+ceiling of 12,000 lines of V. A feature that doesn't fit gets cut, so one5050 person can still read all of it.5151 5252 ## Setting up sync@@ -86,15 +86,23 @@8686 8787 ## Status8888 89-Koh 3 is a rewrite of Koh 2 in V, for macOS and Linux. While it's being built90-the binary is called `koh3`. At the cut-over it becomes `koh`, Koh 2 becomes91-`koh2`, and `koh migrate` converts a Koh 2 repo in place. The specification92-is `/Users/june/Downloads/Koh 3 — God Document.md`.89+Koh 3 is released for macOS and Linux on Intel and ARM64:9390 91+```sh92+curl -fsSL https://koh.asha.software/install.sh | sh93+```94+95+The installer checks the release checksum and offers the optional zsh plugin.96+Koh 2 users can run `koh update` and confirm the upgrade, then `koh migrate`97+in each repository. Save unfinished work with Koh 2 first. Migration keeps98+save IDs and archives the old data in `.koh-v2/`; Koh 3 refuses repository99+commands until migration is complete. The specification is100+`docs/koh-kepr-3-master-spec.md`.101+94102 ## Building95103 96104 ```sh97-v -o build/koh3 src/koh3.v            # or -prod for a release build105+v -o build/koh3 src/koh.v            # or -prod for a release build98106 v test src/ && v test tests/99107 v fmt -verify src/ tests/100108 ```

view file

VERSION+1 −1
@@ -1 +1 @@1-3.4.31+3.4.4

view file

build.sh+26 −0
@@ -0,0 +1,26 @@1+#!/bin/sh2+# Koh's four release targets use the pinned V compiler; Linux binaries are static musl builds.3+set -eu4+[ "$(v version)" = 'V 0.5.2 b9a2d87' ] || { echo 'Koh requires V 0.5.2 b9a2d87' >&2; exit 1; }5+target=${KOH_TARGET:-macos-aarch64}6+mkdir -p build/releases7+case "$target" in8+  macos-aarch64|macos-x86_64)9+    [ "$(uname -s)" = Darwin ] || { echo 'macOS releases require the Apple SDK on macOS' >&2; exit 1; }10+    case "$target" in macos-aarch64) arch=arm64; cpu=arm64 ;; *) arch=x64; cpu=x86_64 ;; esac11+    v -prod -d use_bundled_libgc -arch "$arch" -cflags "-arch $cpu -mmacosx-version-min=11.0" -o "build/releases/koh-$target" src/koh.v12+    codesign -f -s - "build/releases/koh-$target"13+    ;;14+  linux-aarch64|linux-x86_64)15+    command -v zig >/dev/null 2>&1 || { echo 'Linux cross builds require Zig on PATH' >&2; exit 1; }16+    case "$target" in linux-aarch64) arch=arm64; triple=aarch64-linux-musl ;; *) arch=x64; triple=x86_64-linux-musl ;; esac17+    compiler="$PWD/build/cc-$target"18+    printf '#!/bin/sh\nexec zig cc -target %s "$@"\n' "$triple" > "$compiler"19+    chmod 755 "$compiler"20+    v -prod -d use_bundled_libgc -os linux -arch "$arch" -musl -cc "$compiler" -o "build/releases/koh-$target" src/koh.v21+    zig objcopy --strip-all "build/releases/koh-$target" "build/releases/koh-$target.stripped"22+    chmod 755 "build/releases/koh-$target.stripped"23+    mv -f "build/releases/koh-$target.stripped" "build/releases/koh-$target"24+    ;;25+  *) echo "unsupported release target: $target" >&2; exit 1 ;;26+esac

view file

koh.build+4 −0
@@ -0,0 +1,4 @@1+command = sh build.sh2+output = build/releases/koh-{platform}3+platforms = macos-aarch64, macos-x86_64, linux-x86_64, linux-aarch644+lang = shell

view file

shell/koh3.plugin.zsh+7 −3
@@ -27,10 +27,8 @@2727 : ${KOH_PROMPT_CHAR:=→}2828 : ${KOH_DEFAULT_PROMPT:=true}2929 30-# During the transition the Koh 3 binary is koh3; after the cut-over it is koh.3130 _koh_bin() {3231   if [[ -n $KOH_BIN ]]; then print -r -- $KOH_BIN33-  elif (( $+commands[koh3] )); then print koh33432   else print koh3533   fi3634 }@@ -139,6 +137,8 @@139137     'init:create .koh here'140138     'doctor:integrity check and cleanup'141139     'strip:remove a path from history'140+    'build:build from koh.build'141+    'update:update Koh itself'142142     'migrate:convert a Koh 2 repo here'143143     'man:the manual, built in'144144   )@@ -165,8 +165,12 @@165165     steal)           _arguments '--list' '--mine' '--theirs' '--json' ;;166166     strip)           _arguments '--save[one save]:save:_koh3_ids' '*:path:_files' ;;167167     doctor)          _arguments '--backup[copy .koh to]:path:_files' ;;168+    build)           _arguments '*--platform[target platform]:platform:(macos-aarch64 macos-x86_64 linux-aarch64 linux-x86_64)' '--all' ;;169+    update)          _arguments '--check' '--yes' '--json' ;;168170     migrate)         _arguments '--lane[Koh 2 lane]:lane:(dev main)' '*--with[a lost version, from a backup]:file:_files' '--yes[skip the question]' '--json' ;;169171     man)             _describe 'command' commands ;;170172   esac171173 }172-(( $+functions[compdef] )) && compdef _koh3 ${$(_koh_bin):t}174+if (( $+functions[compdef] )); then175+  compdef _koh3 ${$(_koh_bin):t}176+fi

view file

src/cli/cli.v+3 −1
@@ -44,7 +44,9 @@4444 		eprintln(usage_text())4545 		return 24646 	}47-	hand_over(argv)47+	if needs_migration(argv) {48+		return refuse('--json' in argv, 'this is a Koh 2 repo — run "koh migrate" first')49+	}4850 	rest := argv[2..]4951 	match argv[1] {5052 		'--version', '-v' {

view file

src/cli/handover.v+6 −61
@@ -1,69 +1,14 @@1-//! The cut-over hand-off.2-//!3-//! While repos with an unmigrated Koh 2 `.koh/` directory still exist on this4-//! machine, `koh` is Koh 3 but those repos belong to Koh 2. Rather than let a5-//! command fail confusingly, Koh 3 hands it to `koh2` unchanged — same6-//! arguments, same terminal. Agents always type `koh`, and every repo gets the7-//! Koh that owns it.8-//!9-//! `migrate` is the exception: it is how such a repo stops being a Koh 2 repo.10-//! Commands about the machine rather than the repo stay Koh 3's, and a command11-//! Koh 2 reads differently, or lacks, is refused with the way forward.12-1+//! Refuse repository commands until a Koh 2 repository has been migrated.132 module cli143 154 import os16-import src.fmt175 18-// hand_over reports whether this invocation belongs to Koh 2, and if so runs19-// it. It never returns when the hand-off happens.20-pub fn hand_over(argv []string) {21-	if argv.len < 2 {22-		return6+pub fn needs_migration(argv []string) bool {7+	if argv.len < 2 || argv[1] in ['migrate', 'man', 'help', '--help', '-h', '--version', '-v',8+		'remote', 'update'] {9+		return false2310 	}24-	command := argv[1]25-	if command in ['migrate', 'man', 'help', '--help', '-h', '--version', '-v', 'remote', 'update'] {26-		return27-	}28-	if !inside_koh2_repo(os.getwd()) {29-		return30-	}31-	// Koh 2's `view` sets visibility, its `steal` downloads a URL, its `doctor`32-	// checks something else, and it has no `build` or `strip`.33-	if command in ['view', 'steal', 'doctor', 'build', 'strip'] {34-		eprintln('  ${fmt.red(fmt.glyph_fail)} "koh ${command}" needs a Koh 3 repo, and this one is still Koh 2 — run "koh migrate" to convert it')35-		exit(1)36-	}37-	target := koh2_binary()38-	if target == '' {39-		eprintln('  ${fmt.red(fmt.glyph_fail)} this folder holds a Koh 2 repo, and koh2 is not on PATH')40-		eprintln('  install koh2, or run "koh migrate" to convert this repo')41-		exit(1)42-	}43-	// The one notice line the document asks for, on stderr so the output koh244-	// prints (JSON included) reaches the caller unchanged.45-	eprintln('  ${fmt.dim('this is a Koh 2 repo — handing the command to ${os.file_name(target)}')}')46-	// execvp builds argv as [path, ...cmdargs], so the arguments go without47-	// our own argv[0].48-	os.execvp(target, argv[1..]) or {49-		eprintln('  ${fmt.red(fmt.glyph_fail)} could not hand this command to ${target}: ${err.msg()}')50-		exit(1)51-	}52-}53-54-// koh2_binary finds the Koh 2 binary. Before the cutover it is still called55-// `koh`, so that name is a fallback — never this binary itself, which would56-// hand the command straight back to us for ever.57-fn koh2_binary() string {58-	if found := os.find_abs_path_of_executable('koh2') {59-		return found60-	}61-	if legacy := os.find_abs_path_of_executable('koh') {62-		if os.real_path(legacy) != os.real_path(os.executable()) {63-			return legacy64-		}65-	}66-	return ''11+	return inside_koh2_repo(os.getwd())6712 }6813 6914 // inside_koh2_repo looks upward for a `.koh` that is a directory. A `.koh`

view file

src/koh.v+9 −0
@@ -0,0 +1,9 @@1+//! Program entry: hands the command line to the CLI and exits with its code.2+module main3+4+import os5+import src.cli6+7+fn main() {8+	exit(cli.run(os.args))9+}

view file

src/koh3.v+0 −9
@@ -1,9 +0,0 @@1-//! Program entry: hands the command line to the CLI and exits with its code.2-module main3-4-import os5-import src.cli6-7-fn main() {8-	exit(cli.run(os.args))9-}

view file at the parent

tests/cli_test.v+11 −15
@@ -17,7 +17,7 @@1717 1818 fn testsuite_begin() {1919 	os.mkdir_all(os.join_path(root, 'build')) or { panic(err) }20-	build := os.execute('v -o ${q(binary)} ${q(os.join_path(root, 'src', 'koh3.v'))}')20+	build := os.execute('v -o ${q(binary)} ${q(os.join_path(root, 'src', 'koh.v'))}')2121 	if build.exit_code != 0 {2222 		panic('could not build koh3: ${build.output}')2323 	}@@ -204,7 +204,7 @@204204 	for item in ['src', 'vendor', 'docs', 'v.mod', 'VERSION'] {205205 		os.cp_all(os.join_path(root, item), os.join_path(dir, item), true) or { panic(err) }206206 	}207-	write(dir, 'koh.build', 'command = mkdir -p zig-out/bin && v -o zig-out/bin/koh3 src/koh3.v207+	write(dir, 'koh.build', 'command = mkdir -p zig-out/bin && v -o zig-out/bin/koh3 src/koh.v208208 output = zig-out/bin/koh3209209 platforms = ${this_platform()}210210 ')@@ -1296,7 +1296,7 @@12961296 	assert !os.exists(remote)12971297 }12981298 1299-fn test_man_and_koh2_handoff() {1299+fn test_man_and_koh2_migration_gate() {13001300 	dir := temp_repo('man')13011301 	home := temp_repo('man-home')13021302 	defer {@@ -1326,19 +1326,15 @@13261326 	fake_koh := os.join_path(fake, 'koh2')13271327 	os.write_file(fake_koh, '#!/bin/sh\nprintf "handoff:%s\\n" "$*"\nexit 7\n') or { panic(err) }13281328 	os.chmod(fake_koh, 0o755) or { panic(err) }1329-	handoff := run_home_with_path(legacy, home, fake, 'status', '--json')1330-	assert handoff.code == 71331-	assert handoff.out.contains('handoff:status --json')1332-	// One notice line, on stderr, so koh2's own output stays as it was.1333-	assert handoff.err.contains('Koh 2 repo')1334-	assert handoff.err.trim_space().split_into_lines().len == 11335-	assert handoff.out.trim_space() == 'handoff:status --json'1336-	// A command Koh 2 reads differently, or lacks, is refused rather than handed off.1337-	for command in ['view', 'strip', 'build', 'doctor', 'steal'] {1338-		refused := run_home_with_path(legacy, home, fake, command)1329+	for command in ['init', 'save', 'checkpoint', 'status', 'diff', 'undo', 'revert', 'load', 'flag',1330+		'pluck', 'peek', 'offer', 'view', 'strip', 'build', 'doctor', 'steal'] {1331+		refused := run_home_with_path(legacy, home, fake, command, '--json')13391332 		assert refused.code == 1, command1340-		assert !refused.out.contains('handoff'), command1341-		assert refused.err.contains('"koh ${command}" needs a Koh 3 repo, and this one is still Koh 2 — run "koh migrate"'), refused.err1333+		assert refused.out.trim_space() == '', command1334+		assert refused.err.contains('koh migrate'), refused.err1335+		assert !refused.err.contains('handing'), refused.err1336+		assert os.is_dir(os.join_path(legacy, '.koh'))1337+		assert !os.exists(os.join_path(legacy, '.koh-v2'))13421338 	}13431339 	// And one that belongs to the machine, not the repo, is Koh 3's own.13441340 	remote := run_home_with_path(legacy, home, fake, 'remote')

view file

v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 	name: 'koh3'3-	version: '3.4.3'3+	version: '3.4.4'44 	description: 'One SQLite file per repo, one line of history.'55 	license: 'MIT'66 }

view file