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
↓ koh-6123d2cf.tar.gzatlas at this save
files changed11 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 −1README.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 ```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+esackoh.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 = shellshell/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+fisrc/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' {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`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+}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-}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')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 }