$ cd /home/martin/workspace/prototypes/boot-to-claude git commit -m "$(cat <<'EOF' Phase T9: docs + cleanup README's toolchain-requirements section now reflects reality after Phase T8: nasm/ld/objcopy aren't needed at all, and host gcc is a one-time-only bootstrap dependency for toolchain/'s own source, never invoked on OS code. Added a docs/toolchain.md link and toolchain/ entry to the repo-layout table, and listed bootstrap_selfhost.sh / run_toolchain_tests.sh / tests/toolchain/ alongside the existing tools/tests entries. docs/toolchain.md itself was already finalized incrementally across T1-T8, so no further changes there. Final verification for this phase (and the project): a full clean `make`, tools/run_tests.sh, tools/run_toolchain_tests.sh, and an explicit Stage2==Stage3 cmp for as/cc/ld - all green from one build. This closes out the self-hosted-toolchain project: every line of this OS, and the toolchain that builds it, is Claude-authored. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> EOF )"
[main 900c4cf] Phase T9: docs + cleanup 1 file changed, 23 insertions(+), 8 deletions(-)
{
"questions": [
{
"question": "What does \"install and test the self-hosted compiler inside tinyos\" mean concretely? The self-hosted CC currently runs on the host (Linux) to build the OS — it doesn't run as a program inside the OS itself, and tinyos has no mechanism today to load/execute a native program from disk (the shell's `build`/`exec` only run the bytecode VM, not real machine code).",
"header": "Scope",
"multiSelect": false,
"options": [
{
"label": "Build a real exec/loader path (recommended)",
"description": "Add a minimal kernel loader that can load a flat binary from tinyfs and jump into it, port CC (and AS/LD if needed) to a new --target=tinyos mode using tinyfs + kmalloc instead of Linux syscalls, and add a shell command to compile+run a C program from within tinyos. This is a large, multi-phase undertaking on the scale of T1-T9 combined."
},
{
"label": "Just copy the host CC binary onto the disk image",
"description": "Seed build/toolchain/cc (and as/ld) as files on tinyfs via mkfs_tinyfs, so they're \"installed\" on the disk image, even though tinyos can't actually execute them (no ELF/flat-binary loader exists) — this would not be meaningfully testable from the shell."
},
{
"label": "Something else — let me clarify",
"description": "None of the above matches what I meant."
}
]
}
]
}The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.
$ cd /home/martin/workspace/prototypes/boot-to-claude echo "=== serial.c ==="; cat kernel/serial.c echo "=== serial.h ==="; cat kernel/serial.h 2>/dev/null
=== serial.c ===
#include "serial.h"
#include <stdint.h>
#include "builtins.h"
#define COM1 0x3f8
/* Thin names over the compiler's fixed port-I/O builtins (see
* docs/toolchain.md) - kept so the call sites below read the same as
* before rather than spelling out __builtin_* everywhere. */
static inline void outb(uint16_t port, uint8_t val) { __builtin_outb(port, val); }
static inline uint8_t inb(uint16_t port) { return __builtin_inb(port); }
void serial_init(void) {
outb(COM1 + 1, 0x00); /* disable interrupts */
outb(COM1 + 3, 0x80); /* enable DLAB */
outb(COM1 + 0, 0x03); /* divisor low byte: 38400 baud */
outb(COM1 + 1, 0x00); /* divisor high byte */
outb(COM1 + 3, 0x03); /* 8 bits, no parity, one stop bit */
outb(COM1 + 2, 0xC7); /* enable FIFO, clear, 14-byte threshold */
outb(COM1 + 4, 0x0B); /* IRQs disabled, RTS/DSR set */
}
static int transmit_empty(void) {
return inb(COM1 + 5) & 0x20;
}
void serial_putc(char c) {
while (!transmit_empty()) {}
outb(COM1, (uint8_t)c);
}
void serial_write(const char *s) {
while (*s) {
serial_putc(*s++);
}
}
=== serial.h ===
#pragma once
void serial_init(void);
void serial_putc(char c);
void serial_write(const char *s);$ cd /home/martin/workspace/prototypes/boot-to-claude echo "=== keyboard.h ==="; cat kernel/keyboard.h 2>/dev/null echo "=== keyboard.c (head) ==="; sed -n '1,60p' kernel/keyboard.c echo "=== shell.c ==="; cat kernel/shell/shell.c
=== keyboard.h ===
#pragma once
/* Blocks until a key is pressed and returns its ASCII value. Polled PS/2,
* scancode set 1, US layout subset - no interrupts, matching the rest of
* this single-tasking kernel. */
char keyboard_getchar(void);
=== keyboard.c (head) ===
#include "keyboard.h"
#include <stddef.h>
#include <stdint.h>
#include "builtins.h"
#define KBD_DATA 0x60
#define KBD_STATUS 0x64
#define SC_LSHIFT 0x2A
#define SC_RSHIFT 0x36
static inline uint8_t inb(uint16_t port) { return __builtin_inb(port); }
/* Scancode set 1, indexed by make code. 0 = unmapped/non-printable. */
static const char lower_table[] = {
0, 27, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '\b',
'\t', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\n',
0 /*ctrl*/, 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`',
0 /*lshift*/, '\\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/',
0 /*rshift*/, '*', 0 /*alt*/, ' ',
};
static const char upper_table[] = {
0, 27, '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '\b',
'\t', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '\n',
0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', '~',
0, '|', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?',
0, '*', 0, ' ',
};
#define TABLE_LEN (sizeof(lower_table) / sizeof(lower_table[0]))
static int shift_down = 0;
static int scancode_ready(void) {
return inb(KBD_STATUS) & 1;
}
char keyboard_getchar(void) {
for (;;) {
while (!scancode_ready()) {}
uint8_t sc = inb(KBD_DATA);
int release = sc & 0x80;
uint8_t code = sc & 0x7F;
if (code == SC_LSHIFT || code == SC_RSHIFT) {
shift_down = !release;
continue;
}
if (release || code >= TABLE_LEN) {
continue;
}
char c = shift_down ? upper_table[code] : lower_table[code];
if (c == 0) {
continue;
}
=== shell.c ===
#include "shell.h"
#include <stdint.h>
#include "builtins.h"
#include "console.h"
#include "fs/tinyfs.h"
#include "keyboard.h"
#include "lang/compiler/codegen.h"
#include "lang/compiler/parser.h"
#include "lang/script/eval.h"
#include "lang/vm.h"
#include "serial.h"
#include "string.h"
#define LINE_MAX 128
static inline void outb(uint16_t port, uint8_t val) { __builtin_outb(port, val); }
static inline uint8_t inb(uint16_t port) { return __builtin_inb(port); }
static void echo_char(char c) {
console_putc(c);
serial_putc(c);
}
static void print_line(const char *s) {
console_write(s);
console_write("\n");
serial_write(s);
serial_write("\n");
}
static int read_line(char *buf, int max_len) {
int len = 0;
for (;;) {
char c = keyboard_getchar();
if (c == '\n') {
echo_char('\n');
buf[len] = '\0';
return len;
} else if (c == '\b') {
if (len > 0) {
len--;
echo_char('\b');
echo_char(' ');
echo_char('\b');
}
} else if (c >= 32 && c < 127 && len < max_len - 1) {
buf[len++] = c;
echo_char(c);
}
}
}
static void skip_spaces(const char **p) {
while (**p == ' ') {
(*p)++;
}
}
static int next_word(const char **p, char *out, int max_len) {
skip_spaces(p);
int n = 0;
while (**p && **p != ' ' && n < max_len - 1) {
out[n++] = *(*p)++;
}
out[n] = '\0';
return n;
}
static void cmd_help(void) {
print_line("commands: ls, cat <file>, write <file> <text>, rm <file>, run <file>,");
print_line(" build <src> <out>, exec <file>, help, clear, reboot");
}
static void cmd_ls(void) {
tinyfs_dirent_t entries[TINYFS_NUM_INODES];
int n = tinyfs_list(entries, TINYFS_NUM_INODES);
char numbuf[16];
for (int i = 0; i < n; i++) {
console_write(entries[i].name);
console_write(" ");
console_write(itoa((int)entries[i].size, numbuf, 10));
console_write("\n");
serial_write(entries[i].name);
serial_write(" ");
serial_write(itoa((int)entries[i].size, numbuf, 10));
serial_write("\n");
}
}
static void cmd_cat(const char *name) {
static char buf[TINYFS_MAX_FILE_SIZE + 1];
uint32_t len = 0;
if (tinyfs_read(name, buf, sizeof(buf) - 1, &len) != 0) {
print_line("cat: no such file");
return;
}
buf[len] = '\0';
console_write(buf);
serial_write(buf);
if (len == 0 || buf[len - 1] != '\n') {
console_write("\n");
serial_write("\n");
}
}
static void cmd_write(const char *name, const char *contents) {
tinyfs_create(name); /* ignored if it already exists - write() below overwrites it */
uint32_t len = (uint32_t)strlen(contents);
if (len > TINYFS_MAX_FILE_SIZE) {
print_line("write: content too long");
return;
}
if (tinyfs_write(name, contents, len) != 0) {
print_line("write: failed (no space or bad name)");
return;
}
print_line("write: ok");
}
static void cmd_run(const char *name) {
static char buf[TINYFS_MAX_FILE_SIZE + 1];
uint32_t len = 0;
if (tinyfs_read(name, buf, sizeof(buf) - 1, &len) != 0) {
print_line("run: no such file");
return;
}
buf[len] = '\0';
script_run(buf);
}
static void cmd_build(const char *src_name, const char *out_name) {
static char src[TINYFS_MAX_FILE_SIZE + 1];
uint32_t src_len = 0;
if (tinyfs_read(src_name, src, sizeof(src) - 1, &src_len) != 0) {
print_line("build: no such file");
return;
}
src[src_len] = '\0';
tc_parser_t p;
tc_parser_init(&p, src);
ast_node_t *prog = tc_parse_program(&p);
if (p.error) {
console_write("build: parse error: ");
console_write(p.error_msg);
console_write("\n");
serial_write("build: parse error: ");
serial_write(p.error_msg);
serial_write("\n");
return;
}
static uint8_t out_buf[TINYFS_MAX_FILE_SIZE];
uint32_t out_len = 0;
char err[64];
if (codegen_compile(prog, out_buf, sizeof(out_buf), &out_len, err, sizeof(err)) != 0) {
console_write("build: compile error: ");
console_write(err);
console_write("\n");
serial_write("build: compile error: ");
serial_write(err);
serial_write("\n");
return;
}
tinyfs_create(out_name);
if (tinyfs_write(out_name, out_buf, out_len) != 0) {
print_line("build: failed to write output file");
return;
}
print_line("build: ok");
}
static void cmd_exec(const char *name) {
static uint8_t buf[TINYFS_MAX_FILE_SIZE];
uint32_t len = 0;
if (tinyfs_read(name, buf, sizeof(buf), &len) != 0) {
print_line("exec: no such file");
return;
}
vm_run(buf, len);
}
static void cmd_rm(const char *name) {
if (tinyfs_delete(name) != 0) {
print_line("rm: no such file");
} else {
print_line("rm: ok");
}
}
static void cmd_reboot(void) {
print_line("rebooting...");
uint8_t status;
do {
status = inb(0x64);
} while (status & 0x02);
outb(0x64, 0xFE); /* pulse the keyboard controller's reset line */
for (;;) {
__builtin_hlt();
}
}
void shell_run(void) {
static char line[LINE_MAX];
print_line("tiny-os shell. type 'help' for commands.");
for (;;) {
console_write("> ");
serial_write("> ");
read_line(line, sizeof(line));
const char *p = line;
char cmd[32];
if (next_word(&p, cmd, sizeof(cmd)) == 0) {
continue;
}
if (strcmp(cmd, "help") == 0) {
cmd_help();
} else if (strcmp(cmd, "ls") == 0) {
cmd_ls();
} else if (strcmp(cmd, "clear") == 0) {
console_clear();
} else if (strcmp(cmd, "reboot") == 0) {
cmd_reboot();
} else if (strcmp(cmd, "cat") == 0) {
char name[TINYFS_NAME_MAX];
if (next_word(&p, name, sizeof(name)) == 0) {
print_line("usage: cat <file>");
} else {
cmd_cat(name);
}
} else if (strcmp(cmd, "rm") == 0) {
char name[TINYFS_NAME_MAX];
if (next_word(&p, name, sizeof(name)) == 0) {
print_line("usage: rm <file>");
} else {
cmd_rm(name);
}
} else if (strcmp(cmd, "run") == 0) {
char name[TINYFS_NAME_MAX];
if (next_word(&p, name, sizeof(name)) == 0) {
print_line("usage: run <file>");
} else {
cmd_run(name);
}
} else if (strcmp(cmd, "write") == 0) {
char name[TINYFS_NAME_MAX];
if (next_word(&p, name, sizeof(name)) == 0) {
print_line("usage: write <file> <text>");
} else {
skip_spaces(&p);
cmd_write(name, p);
}
} else if (strcmp(cmd, "build") == 0) {
char src_name[TINYFS_NAME_MAX];
char out_name[TINYFS_NAME_MAX];
if (next_word(&p, src_name, sizeof(src_name)) == 0 ||
next_word(&p, out_name, sizeof(out_name)) == 0) {
print_line("usage: build <src> <out>");
} else {
cmd_build(src_name, out_name);
}
} else if (strcmp(cmd, "exec") == 0) {
char name[TINYFS_NAME_MAX];
if (next_word(&p, name, sizeof(name)) == 0) {
print_line("usage: exec <file>");
} else {
cmd_exec(name);
}
} else {
print_line("unknown command (type 'help')");
}
}
}$ cd /home/martin/workspace/prototypes/boot-to-claude grep -n 'KERNEL_C_SRCS' -A 10 Makefile echo "--- console.h ---" cat kernel/console.h echo "--- tools/run_tests.sh head ---" sed -n '1,35p' tools/run_tests.sh
34:KERNEL_C_SRCS := kernel/kmain.c kernel/console.c kernel/serial.c kernel/gdt.c \
35- kernel/idt.c kernel/string.c kernel/memory.c kernel/selftest.c \
36- kernel/ata.c kernel/fs/tinyfs.c kernel/fs/tinyfs_disk.c \
37- kernel/keyboard.c kernel/shell/shell.c \
38- kernel/lang/lexer.c kernel/lang/script/ast.c \
39- kernel/lang/script/parser.c kernel/lang/script/eval.c \
40- kernel/lang/compiler/parser.c kernel/lang/compiler/codegen.c \
41- kernel/lang/vm.c
42-KERNEL_ASM_SRCS := kernel/entry.S kernel/idt_stubs.S
43-KERNEL_OBJS := $(patsubst kernel/%.S,$(BUILD)/%.o,$(KERNEL_ASM_SRCS)) \
44: $(patsubst kernel/%.c,$(BUILD)/%.o,$(KERNEL_C_SRCS))
45-KERNEL_BIN := $(BUILD)/kernel.bin
46-
47-MKFS_BIN := $(BUILD)/mkfs_tinyfs
48-# host_string.o/host_tinyfs.o: kernel/string.c and kernel/fs/tinyfs.c
49-# compiled a *second* time, natively - host tools are libc-free (see
50-# docs/toolchain.md), same as the kernel itself, and this build has its
51-# own build/fs/tinyfs.o etc for the freestanding kernel target, so these
52-# need the host_ prefix to not collide with those.
53-# tools/host_syscalls_shim.c is gone the same way builtins_shim.c is:
54-# CC intercepts __sys_* calls directly.
--- console.h ---
#pragma once
void console_clear(void);
void console_putc(char c);
void console_write(const char *s);
--- tools/run_tests.sh head ---
#!/usr/bin/env bash
# Boots the kernel headlessly in QEMU, checks the kernel self-test result,
# then drives the interactive shell via QEMU's monitor `sendkey` (see
# tools/qemu_type.py - there's no other way to feed the polled PS/2
# keyboard driver without real hardware) and asserts on serial output.
set -euo pipefail
cd "$(dirname "$0")/.."
make >/dev/null
BUILD=build
LOG="$BUILD/serial.log"
MON="$BUILD/qemu-monitor.sock"
rm -f "$LOG" "$MON"
qemu-system-i386 \
-drive file="$BUILD/disk.img",format=raw \
-display none \
-serial file:"$LOG" \
-monitor unix:"$MON",server,nowait \
-no-reboot &
QEMU_PID=$!
cleanup() {
kill "$QEMU_PID" 2>/dev/null || true
wait "$QEMU_PID" 2>/dev/null || true
}
trap cleanup EXIT
fail() {
echo "run_tests: FAIL - $1"
echo "--- serial log ---"
cat "$LOG" 2>/dev/null || true
exit 1
}- #pragma once - - void serial_init(void); - void serial_putc(char c); - void serial_write(const char *s); + #pragma once + + void serial_init(void); + void serial_putc(char c); + void serial_write(const char *s); + + /* Non-blocking: is there a byte waiting to be read from COM1? */ + int serial_has_char(void); + /* Blocks until a byte arrives on COM1 and returns it. */ + char serial_getc(void);
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/serial.h has been updated successfully. (file state is current in your context — no need to Read it back)
- static int transmit_empty(void) {
- return inb(COM1 + 5) & 0x20;
- }
-
- void serial_putc(char c) {
- while (!transmit_empty()) {}
- outb(COM1, (uint8_t)c);
- }
-
- void serial_write(const char *s) {
- while (*s) {
- serial_putc(*s++);
- }
- }
+ static int transmit_empty(void) {
+ return inb(COM1 + 5) & 0x20;
+ }
+
+ void serial_putc(char c) {
+ while (!transmit_empty()) {}
+ outb(COM1, (uint8_t)c);
+ }
+
+ void serial_write(const char *s) {
+ while (*s) {
+ serial_putc(*s++);
+ }
+ }
+
+ int serial_has_char(void) {
+ return inb(COM1 + 5) & 0x01; /* line status: data ready */
+ }
+
+ char serial_getc(void) {
+ while (!serial_has_char()) {}
+ return (char)inb(COM1);
+ }The file /home/martin/workspace/prototypes/boot-to-claude/kernel/serial.c has been updated successfully. (file state is current in your context — no need to Read it back)
36
37 static int scancode_ready(void) {
38 return inb(KBD_STATUS) & 1;
39 }
40
41 char keyboard_getchar(void) {
42 for (;;) {
43 while (!scancode_ready()) {}
44 uint8_t sc = inb(KBD_DATA);
45
46 int release = sc & 0x80;
47 uint8_t code = sc & 0x7F;
48
49 if (code == SC_LSHIFT || code == SC_RSHIFT) {
50 shift_down = !release;
51 continue;
52 }
53 if (release || code >= TABLE_LEN) {
54 continue;
55 }
56
57 char c = shift_down ? upper_table[code] : lower_table[code];
58 if (c == 0) {
59 continue;
60 }
61 return c;
62 }
63 }
64 - static int scancode_ready(void) {
- return inb(KBD_STATUS) & 1;
- }
-
- char keyboard_getchar(void) {
- for (;;) {
- while (!scancode_ready()) {}
- uint8_t sc = inb(KBD_DATA);
+ static int scancode_ready(void) {
+ return inb(KBD_STATUS) & 1;
+ }
+
+ int keyboard_has_char(void) {
+ return scancode_ready();
+ }
+
+ char keyboard_getchar(void) {
+ for (;;) {
+ while (!scancode_ready()) {}
+ uint8_t sc = inb(KBD_DATA);The file /home/martin/workspace/prototypes/boot-to-claude/kernel/keyboard.c has been updated successfully. (file state is current in your context — no need to Read it back)
- #pragma once - - /* Blocks until a key is pressed and returns its ASCII value. Polled PS/2, - * scancode set 1, US layout subset - no interrupts, matching the rest of - * this single-tasking kernel. */ - char keyboard_getchar(void); + #pragma once + + /* Blocks until a key is pressed and returns its ASCII value. Polled PS/2, + * scancode set 1, US layout subset - no interrupts, matching the rest of + * this single-tasking kernel. */ + char keyboard_getchar(void); + + /* Non-blocking: is a raw scancode byte waiting in the controller? Doesn't + * guarantee keyboard_getchar() returns immediately (a lone modifier-key + * scancode consumes itself with no char produced), only that calling it + * won't busy-wait on *this* byte. */ + int keyboard_has_char(void);
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/keyboard.h has been updated successfully. (file state is current in your context — no need to Read it back)
#pragma once /* Blocks until a character arrives from either the PS/2 keyboard or the * serial port (COM1), whichever comes first - lets a human at the * keyboard and e.g. an AI agent connected over serial both drive the * shell/game interchangeably, matching the kernel's existing * dual-output (console+serial) convention for output. */ char input_getchar(void); /* Free-running counter, incremented once per poll iteration spent * waiting for input in input_getchar(). Not wall-clock time (there's no * timer interrupt in this kernel - see README), but it advances at a * rate that depends on real elapsed wall-clock waiting, so it's a * reasonable low-stakes entropy source for e.g. game.c's RNG seed. */ unsigned int input_ticks(void);
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/input.h (file state is current in your context — no need to Read it back)
#include "input.h"
#include "keyboard.h"
#include "serial.h"
static unsigned int g_ticks;
char input_getchar(void) {
for (;;) {
if (keyboard_has_char()) {
return keyboard_getchar();
}
if (serial_has_char()) {
return serial_getc();
}
g_ticks++;
}
}
unsigned int input_ticks(void) {
return g_ticks;
}File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/input.c (file state is current in your context — no need to Read it back)
- #include "builtins.h" - #include "console.h" - #include "fs/tinyfs.h" - #include "keyboard.h" - #include "lang/compiler/codegen.h" - #include "lang/compiler/parser.h" - #include "lang/script/eval.h" - #include "lang/vm.h" - #include "serial.h" - #include "string.h" + #include "builtins.h" + #include "console.h" + #include "fs/tinyfs.h" + #include "game/game.h" + #include "input.h" + #include "lang/compiler/codegen.h" + #include "lang/compiler/parser.h" + #include "lang/script/eval.h" + #include "lang/vm.h" + #include "serial.h" + #include "string.h"
The file /home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c has been updated successfully. (file state is current in your context — no need to Read it back)
- static int read_line(char *buf, int max_len) {
- int len = 0;
- for (;;) {
- char c = keyboard_getchar();
+ static int read_line(char *buf, int max_len) {
+ int len = 0;
+ for (;;) {
+ char c = input_getchar();The file /home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c has been updated successfully. (file state is current in your context — no need to Read it back)
- static void cmd_help(void) {
- print_line("commands: ls, cat <file>, write <file> <text>, rm <file>, run <file>,");
- print_line(" build <src> <out>, exec <file>, help, clear, reboot");
- }
+ static void cmd_help(void) {
+ print_line("commands: ls, cat <file>, write <file> <text>, rm <file>, run <file>,");
+ print_line(" build <src> <out>, exec <file>, play, help, clear, reboot");
+ }The file /home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c has been updated successfully. (file state is current in your context — no need to Read it back)
- } else if (strcmp(cmd, "clear") == 0) {
- console_clear();
- } else if (strcmp(cmd, "reboot") == 0) {
+ } else if (strcmp(cmd, "clear") == 0) {
+ console_clear();
+ } else if (strcmp(cmd, "play") == 0) {
+ game_run();
+ } else if (strcmp(cmd, "reboot") == 0) {The file /home/martin/workspace/prototypes/boot-to-claude/kernel/shell/shell.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ cat /home/martin/workspace/prototypes/boot-to-claude/kernel/string.h
#pragma once #include <stddef.h> void *memcpy(void *dst, const void *src, size_t n); void *memset(void *dst, int val, size_t n); size_t strlen(const char *s); int strcmp(const char *a, const char *b); char *strchr(const char *s, int c); /* Writes the decimal (base 10) or hex (base 16) representation of value * into buf (caller-provided, must be large enough) and returns buf. */ char *itoa(int value, char *buf, int base);
$ mkdir -p /home/martin/workspace/prototypes/boot-to-claude/kernel/game
(Bash completed with no output)
#pragma once /* A small turn-based, Pokemon-like battle game: pick a starter, fight a * run of wild encounters with a rock-paper-scissors type system, until * either your monster faints (game over) or you defeat enough wild * monsters to become champion. Runs its own input/output loop (dual * keyboard+serial, via input.h - see its comment) and returns to the * caller (the shell) when the run ends. */ void game_run(void);
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/game/game.h (file state is current in your context — no need to Read it back)
#include "game.h"
#include <stdint.h>
#include "console.h"
#include "input.h"
#include "serial.h"
#include "string.h"
#define LINE_MAX 64
#define WIN_STAGE 5 /* defeat this many wild monsters to become champion */
/* --- dual console+serial output, same convention as shell.c --- */
static void w(const char *s) {
console_write(s);
serial_write(s);
}
static void wln(const char *s) {
w(s);
w("\n");
}
static void w_int(int v) {
char buf[16];
w(itoa(v, buf, 10));
}
/* --- line input, same convention as shell.c's read_line (dual keyboard
* +serial via input_getchar - see its comment) --- */
static void echo_char(char c) {
console_putc(c);
serial_putc(c);
}
static int read_line(char *buf, int max_len) {
int len = 0;
for (;;) {
char c = input_getchar();
if (c == '\n') {
echo_char('\n');
buf[len] = '\0';
return len;
} else if (c == '\b') {
if (len > 0) {
len--;
echo_char('\b');
echo_char(' ');
echo_char('\b');
}
} else if (c >= 32 && c < 127 && len < max_len - 1) {
buf[len++] = c;
echo_char(c);
}
}
}
/* --- a small xorshift32 PRNG, perturbed each draw with input_ticks()
* (see input.h) - there's no timer interrupt in this kernel, so real
* elapsed-wait-time jitter is the only entropy source available. */
static uint32_t rng_state = 2463534242u; /* must be nonzero */
static uint32_t rng_next(void) {
rng_state ^= input_ticks() + 1;
rng_state ^= rng_state << 13;
rng_state ^= rng_state >> 17;
rng_state ^= rng_state << 5;
return rng_state;
}
static int rng_range(int lo, int hi) { /* inclusive */
return lo + (int)(rng_next() % (uint32_t)(hi - lo + 1));
}
/* --- game data --- */
typedef enum { TYPE_FIRE, TYPE_WATER, TYPE_GRASS } mon_type_t;
typedef struct {
const char *name;
int power;
} move_t;
typedef struct {
const char *name;
mon_type_t type;
int max_hp;
move_t moves[3];
} species_t;
#define NUM_SPECIES 3
static const species_t SPECIES[NUM_SPECIES] = {
{ "Emberpup", TYPE_FIRE, 28, { { "Ember", 7 }, { "Scratch", 5 }, { "Flame Burst", 10 } } },
{ "Aqualing", TYPE_WATER, 30, { { "Water Jet", 7 }, { "Tail Slap", 5 }, { "Tide Wave", 10 } } },
{ "Leafling", TYPE_GRASS, 32, { { "Vine Whip", 7 }, { "Tackle", 5 }, { "Leaf Storm", 10 } } },
};
/* Rock-paper-scissors: fire > grass > water > fire. */
static int type_beats(mon_type_t a, mon_type_t d) {
return (a == TYPE_FIRE && d == TYPE_GRASS) ||
(a == TYPE_GRASS && d == TYPE_WATER) ||
(a == TYPE_WATER && d == TYPE_FIRE);
}
static int compute_damage(mon_type_t attacker_type, mon_type_t defender_type, int power) {
int dmg = power;
if (type_beats(attacker_type, defender_type)) {
w("It's super effective!\n");
dmg = dmg * 3 / 2;
} else if (type_beats(defender_type, attacker_type)) {
w("It's not very effective...\n");
dmg = dmg / 2;
}
dmg += rng_range(-2, 2);
if (dmg < 1) {
dmg = 1;
}
return dmg;
}
/* --- status line helpers --- */
static void print_hp(const char *label, const char *name, int hp, int max_hp) {
w(label);
w(name);
w(": HP ");
w_int(hp);
w("/");
w_int(max_hp);
w("\n");
}
/* Parses a line already trimmed of leading spaces: a leading digit is a
* numbered menu choice (1-9), otherwise "run"/"quit" are recognized as
* words - covers both a human pressing a number key and an AI agent
* typing a full word, whichever is more natural for it. Returns the
* choice as 1-9, 0 for "run", -1 for "quit", -2 if unrecognized. */
static int parse_choice(const char *line) {
while (*line == ' ') {
line++;
}
if (*line >= '1' && *line <= '9') {
return *line - '0';
}
if (strcmp(line, "run") == 0) {
return 0;
}
if (strcmp(line, "quit") == 0) {
return -1;
}
return -2;
}
/* Returns the chosen starter index (0..NUM_SPECIES-1), or -1 if the
* player typed "quit". */
static int choose_starter(void) {
char line[LINE_MAX];
wln("Choose your starter:");
for (int i = 0; i < NUM_SPECIES; i++) {
w_int(i + 1);
w(") ");
w(SPECIES[i].name);
w(" (");
w(SPECIES[i].type == TYPE_FIRE ? "Fire" : SPECIES[i].type == TYPE_WATER ? "Water" : "Grass");
wln(")");
}
for (;;) {
w("> ");
read_line(line, sizeof(line));
int choice = parse_choice(line);
if (choice == -1) {
return -1;
}
if (choice >= 1 && choice <= NUM_SPECIES) {
return choice - 1;
}
wln("invalid choice - type a number, 'run', or 'quit'");
}
}
void game_run(void) {
char line[LINE_MAX];
wln("=== Tiny Monsters ===");
wln("Defeat 5 wild monsters to become champion. Type 'quit' any time to leave.");
int player_idx = choose_starter();
if (player_idx < 0) {
wln("Left the game.");
return;
}
const species_t *player = &SPECIES[player_idx];
int player_hp = player->max_hp;
int stage = 1;
for (;;) {
int wild_idx = rng_range(0, NUM_SPECIES - 1);
const species_t *wild = &SPECIES[wild_idx];
int wild_max_hp = wild->max_hp + (stage - 1) * 4;
int wild_hp = wild_max_hp;
w("=== A wild ");
w(wild->name);
w(" appears! (stage ");
w_int(stage);
wln(")");
for (;;) {
print_hp("Your ", player->name, player_hp, player->max_hp);
print_hp("Wild ", wild->name, wild_hp, wild_max_hp);
for (int i = 0; i < 3; i++) {
w_int(i + 1);
w(") ");
w(player->moves[i].name);
w(" (");
w_int(player->moves[i].power);
wln(")");
}
wln("4) Run");
w("> ");
read_line(line, sizeof(line));
int choice = parse_choice(line);
if (choice == -1) {
wln("Left the game.");
return;
}
if (choice == 0 || choice == 4) {
wln("You ran away!");
break; /* next encounter, same stage */
}
if (choice < 1 || choice > 3) {
wln("invalid choice - type 1-3 for a move, 'run', or 'quit'");
continue;
}
const move_t *mv = &player->moves[choice - 1];
w("Your ");
w(player->name);
w(" uses ");
w(mv->name);
wln("!");
int dmg = compute_damage(player->type, wild->type, mv->power);
wild_hp -= dmg;
if (wild_hp < 0) {
wild_hp = 0;
}
w("Wild ");
w(wild->name);
w(" takes ");
w_int(dmg);
w(" damage. (HP ");
w_int(wild_hp);
w("/");
w_int(wild_max_hp);
wln(")");
if (wild_hp <= 0) {
w("Wild ");
w(wild->name);
wln(" fainted! You win!");
stage++;
if (stage > WIN_STAGE) {
w("*** You defeated ");
w_int(WIN_STAGE);
wln(" wild monsters - you are the Champion! ***");
return;
}
break; /* next encounter */
}
const move_t *wmv = &wild->moves[rng_range(0, 2)];
w("Wild ");
w(wild->name);
w(" uses ");
w(wmv->name);
wln("!");
int wdmg = compute_damage(wild->type, player->type, wmv->power);
player_hp -= wdmg;
if (player_hp < 0) {
player_hp = 0;
}
w("Your ");
w(player->name);
w(" takes ");
w_int(wdmg);
w(" damage. (HP ");
w_int(player_hp);
w("/");
w_int(player->max_hp);
wln(")");
if (player_hp <= 0) {
w("Your ");
w(player->name);
w(" fainted... Game over! You reached stage ");
w_int(stage);
wln(".");
return;
}
}
}
}File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/kernel/game/game.c (file state is current in your context — no need to Read it back)
34 KERNEL_C_SRCS := kernel/kmain.c kernel/console.c kernel/serial.c kernel/gdt.c \ 35 kernel/idt.c kernel/string.c kernel/memory.c kernel/selftest.c \ 36 kernel/ata.c kernel/fs/tinyfs.c kernel/fs/tinyfs_disk.c \ 37 kernel/keyboard.c kernel/shell/shell.c \ 38 kernel/lang/lexer.c kernel/lang/script/ast.c \ 39 kernel/lang/script/parser.c kernel/lang/script/eval.c \ 40 kernel/lang/compiler/parser.c kernel/lang/compiler/codegen.c \ 41 kernel/lang/vm.c 42 KERNEL_ASM_SRCS := kernel/entry.S kernel/idt_stubs.S 43 KERNEL_OBJS := $(patsubst kernel/%.S,$(BUILD)/%.o,$(KERNEL_ASM_SRCS)) \
- KERNEL_C_SRCS := kernel/kmain.c kernel/console.c kernel/serial.c kernel/gdt.c \ - kernel/idt.c kernel/string.c kernel/memory.c kernel/selftest.c \ - kernel/ata.c kernel/fs/tinyfs.c kernel/fs/tinyfs_disk.c \ - kernel/keyboard.c kernel/shell/shell.c \ - kernel/lang/lexer.c kernel/lang/script/ast.c \ - kernel/lang/script/parser.c kernel/lang/script/eval.c \ - kernel/lang/compiler/parser.c kernel/lang/compiler/codegen.c \ - kernel/lang/vm.c + KERNEL_C_SRCS := kernel/kmain.c kernel/console.c kernel/serial.c kernel/gdt.c \ + kernel/idt.c kernel/string.c kernel/memory.c kernel/selftest.c \ + kernel/ata.c kernel/fs/tinyfs.c kernel/fs/tinyfs_disk.c \ + kernel/keyboard.c kernel/input.c kernel/shell/shell.c \ + kernel/game/game.c \ + kernel/lang/lexer.c kernel/lang/script/ast.c \ + kernel/lang/script/parser.c kernel/lang/script/eval.c \ + kernel/lang/compiler/parser.c kernel/lang/compiler/codegen.c \ + kernel/lang/vm.c
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
$ grep -n 'BUILD_SUBDIRS' /home/martin/workspace/prototypes/boot-to-claude/Makefile
66:BUILD_SUBDIRS := $(BUILD) $(BUILD)/toolchain $(BUILD)/fs $(BUILD)/shell \ 69:$(BUILD_SUBDIRS): 107:$(BUILD)/%.o: kernel/%.S | $(BUILD_SUBDIRS) $(AS) 110:$(BUILD)/%.S: kernel/%.c | $(BUILD_SUBDIRS) $(CC) 113:$(BUILD)/%.o: $(BUILD)/%.S | $(BUILD_SUBDIRS) $(AS)
66 BUILD_SUBDIRS := $(BUILD) $(BUILD)/toolchain $(BUILD)/fs $(BUILD)/shell \ 67 $(BUILD)/lang $(BUILD)/lang/script $(BUILD)/lang/compiler 68
- BUILD_SUBDIRS := $(BUILD) $(BUILD)/toolchain $(BUILD)/fs $(BUILD)/shell \ - $(BUILD)/lang $(BUILD)/lang/script $(BUILD)/lang/compiler + BUILD_SUBDIRS := $(BUILD) $(BUILD)/toolchain $(BUILD)/fs $(BUILD)/shell \ + $(BUILD)/game $(BUILD)/lang $(BUILD)/lang/script $(BUILD)/lang/compiler
The file /home/martin/workspace/prototypes/boot-to-claude/Makefile has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude timeout 120 make 2>&1 | tail -60
mkdir -p build/game build/toolchain/cc -Ikernel -Itoolchain/include -o build/serial.S kernel/serial.c build/toolchain/as -f elf32 -o build/serial.o build/serial.S build/toolchain/cc -Ikernel -Itoolchain/include -o build/keyboard.S kernel/keyboard.c build/toolchain/as -f elf32 -o build/keyboard.o build/keyboard.S build/toolchain/cc -Ikernel -Itoolchain/include -o build/input.S kernel/input.c build/toolchain/as -f elf32 -o build/input.o build/input.S build/toolchain/cc -Ikernel -Itoolchain/include -o build/shell/shell.S kernel/shell/shell.c build/toolchain/as -f elf32 -o build/shell/shell.o build/shell/shell.S build/toolchain/cc -Ikernel -Itoolchain/include -o build/game/game.S kernel/game/game.c build/toolchain/as -f elf32 -o build/game/game.o build/game/game.S build/toolchain/ld -T kernel/link.ld --format bin -o build/kernel.bin build/entry.o build/idt_stubs.o build/kmain.o build/console.o build/serial.o build/gdt.o build/idt.o build/string.o build/memory.o build/selftest.o build/ata.o build/fs/tinyfs.o build/fs/tinyfs_disk.o build/keyboard.o build/input.o build/shell/shell.o build/game/game.o build/lang/lexer.o build/lang/script/ast.o build/lang/script/parser.o build/lang/script/eval.o build/lang/compiler/parser.o build/lang/compiler/codegen.o build/lang/vm.o dd if=/dev/zero of=build/disk.img bs=512 count=4096 status=none dd if=build/boot.bin of=build/disk.img conv=notrunc status=none dd if=build/stage2.bin of=build/disk.img bs=512 seek=1 conv=notrunc status=none dd if=build/kernel.bin of=build/disk.img bs=512 seek=5 conv=notrunc status=none build/mkfs_tinyfs build/disk.img tests/hello.tsh:hello.tsh tests/hello.tc:hello.tc mkfs_tinyfs: done rm build/shell/shell.S build/serial.S build/input.S build/game/game.S build/keyboard.S
$ cd /home/martin/workspace/prototypes/boot-to-claude timeout 60 ./tools/run_tests.sh 2>&1 | tail -15
run_tests: PASS qemu-system-i386: terminating on signal 15 from pid 1696727 (bash)
$ cat /home/martin/workspace/prototypes/boot-to-claude/tools/qemu_type.py
#!/usr/bin/env python3
"""Types text into a running QEMU guest via its HMP monitor's `sendkey`
command, over a unix socket (-monitor unix:PATH,server,nowait). Used by
tools/run_tests.sh to drive the kernel shell headlessly - there is no
other way to feed the polled PS/2 keyboard driver without real hardware
or a display.
"""
import socket
import sys
import time
KEYMAP = {
" ": "spc",
".": "dot",
"\n": "ret",
"-": "minus",
"_": "shift-minus",
",": "comma",
"/": "slash",
}
def key_name(c):
if c in KEYMAP:
return KEYMAP[c]
if c.isalpha():
return c.lower()
if c.isdigit():
return c
raise ValueError(f"qemu_type: no key mapping for {c!r}")
def main():
if len(sys.argv) != 3:
print(f"usage: {sys.argv[0]} <monitor-socket> <text>", file=sys.stderr)
sys.exit(1)
sock_path, text = sys.argv[1], sys.argv[2]
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(sock_path)
time.sleep(0.2)
s.recv(4096) # monitor banner
for c in text:
s.sendall(f"sendkey {key_name(c)}\n".encode())
time.sleep(0.05)
s.recv(4096)
s.close()
if __name__ == "__main__":
main()$ cd /home/martin/workspace/prototypes/boot-to-claude
pkill -f 'qemu-system-i386.*game_test' 2>/dev/null
rm -f build/game_test.sock
timeout 40 qemu-system-i386 \
-drive file=build/disk.img,format=raw \
-display none \
-serial tcp:127.0.0.1:4555,server,nowait \
-no-reboot >/tmp/qemu_game_test.log 2>&1 &
QEMU_PID=$!
echo "QEMU PID=$QEMU_PID"
sleep 2
echo "started"Exit code 144
$ cd /home/martin/workspace/prototypes/boot-to-claude
pkill -f 'qemu-system-i386.*4555' 2>/dev/null; sleep 1
qemu-system-i386 \
-drive file=build/disk.img,format=raw \
-display none \
-serial tcp:127.0.0.1:4555,server,nowait \
-no-reboot > /tmp/qemu_game_test.log 2>&1Command running in background with ID: brqrwkx0j. Output is being written to: /tmp/claude-1001/-home-martin-workspace-prototypes-boot-to-claude/03468d1c-5b29-4e24-8f0d-2a7435d227ab/tasks/brqrwkx0j.output. You will be notified when it completes. To check interim output, use Read on that file path. Session cwd remains /home/martin/workspace/prototypes/boot-to-claude; directory changes made by the backgrounded command do not apply to subsequent commands.
$ sleep 2
python3 - <<'EOF'
import socket, time
s = socket.create_connection(("127.0.0.1", 4555), timeout=10)
s.settimeout(3)
def drain(t=1.5):
buf = b""
end = time.time() + t
while time.time() < end:
try:
chunk = s.recv(4096)
if not chunk:
break
buf += chunk
except socket.timeout:
break
return buf.decode(errors="replace")
print("--- boot output ---")
print(drain(6))
EOFExit code 1
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
File "/usr/lib/python3.13/socket.py", line 864, in create_connection
raise exceptions[0]
File "/usr/lib/python3.13/socket.py", line 849, in create_connection
sock.connect(sa)
~~~~~~~~~~~~^^^^
ConnectionRefusedError: [Errno 111] Connection refused$ cat /tmp/qemu_game_test.log
Exit code 1 cat: /tmp/qemu_game_test.log: No such file or directory (os error 2)
$ cat "/tmp/claude-1001/-home-martin-workspace-prototypes-boot-to-claude/03468d1c-5b29-4e24-8f0d-2a7435d227ab/tasks/brqrwkx0j.output"
(Bash completed with no output)
$ cd /home/martin/workspace/prototypes/boot-to-claude
timeout 3 qemu-system-i386 \
-drive file=build/disk.img,format=raw \
-display none \
-serial tcp:127.0.0.1:4555,server,nowait \
-no-reboot
echo "exit=$?"qemu-system-i386: terminating on signal 15 from pid 1696903 (timeout) exit=124
$ pkill -f 'qemu-system-i386.*4555' 2>/dev/null; sleep 1; ss -tlnp 2>/dev/null | grep 4555; echo "port check done"
Exit code 144
$ pkill -f 'qemu-system-i386.*4555' || true
Exit code 144
$ ps aux | grep qemu-system | grep -v grep
(Bash completed with no output)
$ cd /home/martin/workspace/prototypes/boot-to-claude
qemu-system-i386 \
-drive file=build/disk.img,format=raw \
-display none \
-serial tcp:127.0.0.1:4555,server,nowait \
-no-rebootCommand running in background with ID: buxpmrew4. Output is being written to: /tmp/claude-1001/-home-martin-workspace-prototypes-boot-to-claude/03468d1c-5b29-4e24-8f0d-2a7435d227ab/tasks/buxpmrew4.output. You will be notified when it completes. To check interim output, use Read on that file path. Session cwd remains /home/martin/workspace/prototypes/boot-to-claude; directory changes made by the backgrounded command do not apply to subsequent commands.
$ sleep 3
python3 - <<'PYEOF'
import socket, time
s = socket.create_connection(("127.0.0.1", 4555), timeout=10)
s.settimeout(3)
def drain(t=1.5):
buf = b""
end = time.time() + t
while time.time() < end:
try:
chunk = s.recv(4096)
if not chunk:
break
buf += chunk
except socket.timeout:
break
return buf.decode(errors="replace")
print("--- boot output ---")
print(drain(6))
PYEOF--- boot output ---
$ cat "/tmp/claude-1001/-home-martin-workspace-prototypes-boot-to-claude/03468d1c-5b29-4e24-8f0d-2a7435d227ab/tasks/buxpmrew4.output" 2>&1 ps aux | grep qemu-system | grep -v grep
martin 1696988 0.0 0.0 13312 3908 ? Ss 07:12 0:00 /bin/bash -c source /home/martin/.claude/shell-snapshots/snapshot-bash-1784610228462-5rmhnd.sh 2>/dev/null || true && shopt -u extglob 2>/dev/null || true && eval 'cd /home/martin/workspace/prototypes/boot-to-claude qemu-system-i386 \ -drive file=build/disk.img,format=raw \ -display none \ -serial tcp:127.0.0.1:4555,server,nowait \ -no-reboot' < /dev/null && pwd -P >| /tmp/claude-d493-cwd martin 1696990 100 0.1 1741904 49936 ? Sl 07:12 0:15 qemu-system-i386 -drive file=build/disk.img,format=raw -display none -serial tcp:127.0.0.1:4555,server,nowait -no-reboot
$ python3 - <<'PYEOF'
import socket, time
try:
s = socket.create_connection(("127.0.0.1", 4555), timeout=10)
except Exception as e:
print("connect failed:", e)
raise SystemExit
s.settimeout(2)
buf = b""
for _ in range(10):
try:
chunk = s.recv(4096)
if not chunk:
print("connection closed")
break
buf += chunk
except socket.timeout:
print("timeout waiting, total so far:", len(buf))
print(repr(buf.decode(errors="replace")))
s.close()
PYEOFtimeout waiting, total so far: 0 timeout waiting, total so far: 0 timeout waiting, total so far: 0 timeout waiting, total so far: 0 timeout waiting, total so far: 0 timeout waiting, total so far: 0 timeout waiting, total so far: 0 timeout waiting, total so far: 0 timeout waiting, total so far: 0 timeout waiting, total so far: 0 ''
$ python3 - <<'PYEOF'
import socket, time
s = socket.create_connection(("127.0.0.1", 4555), timeout=10)
s.settimeout(2)
def drain(t=2.0):
buf = b""
end = time.time() + t
while time.time() < end:
try:
chunk = s.recv(4096)
if not chunk:
break
buf += chunk
except socket.timeout:
break
return buf
s.sendall(b"help\n")
out = drain(3)
print(repr(out.decode(errors="replace")))
s.close()
PYEOF'help\ncommands: ls, cat <file>, write <file> <text>, rm <file>, run <file>,\n build <src> <out>, exec <file>, play, help, clear, reboot\n> '
$ python3 - <<'PYEOF'
import socket, time
s = socket.create_connection(("127.0.0.1", 4555), timeout=10)
s.settimeout(2)
def drain(t=2.0):
buf = b""
end = time.time() + t
while time.time() < end:
try:
chunk = s.recv(4096)
if not chunk:
break
buf += chunk
except socket.timeout:
break
return buf.decode(errors="replace")
def send(line):
s.sendall((line + "\n").encode())
return drain(2.0)
print("=== play ===")
print(send("play"))
print("=== choose starter 1 ===")
print(send("1"))
print("=== move 1 ===")
print(send("1"))
print("=== move 1 again ===")
print(send("1"))
print("=== quit ===")
print(send("quit"))
s.close()
PYEOF=== play === play === Tiny Monsters === Defeat 5 wild monsters to become champion. Type 'quit' any time to leave. Choose your starter: 1) Emberpup (Fire) 2) Aqualing (Water) 3) Leafling (Grass) > === choose starter 1 === 1 === A wild Aqualing appears! (stage 1) Your Emberpup: HP 28/28 Wild Aqualing: HP 30/30 1) Ember (7) 2) Scratch (5) 3) Flame Burst (10) 4) Run > === move 1 === 1 Your Emberpup uses Ember! It's not very effective... Wild Aqualing takes 1 damage. (HP 29/30) Wild Aqualing uses �! It's super effective! Your Emberpup takes 40 damage. (HP 0/28) Your Emberpup fainted... Game over! You reached stage 1. > === move 1 again === 1 unknown command (type 'help') > === quit === quit unknown command (type 'help') >
- static int rng_range(int lo, int hi) { /* inclusive */
- return lo + (int)(rng_next() % (uint32_t)(hi - lo + 1));
- }
+ static int rng_range(int lo, int hi) { /* inclusive */
+ /* CC's codegen only ever emits signed idiv, never true unsigned
+ * division (see docs/toolchain.md) - the rest of the kernel avoids
+ * this by construction (nothing else divides a value that could
+ * have the sign bit set), but rng_next()'s raw uint32_t output can.
+ * Left unmasked, a result with the sign bit set gets `%`'d as if
+ * negative, producing a negative "random" index and an out-of-bounds
+ * moves[] read (garbage move name, huge damage). Clearing the sign
+ * bit first keeps the value in idiv's safe signed range. */
+ uint32_t r = rng_next() & 0x7FFFFFFFu;
+ return lo + (int)(r % (uint32_t)(hi - lo + 1));
+ }The file /home/martin/workspace/prototypes/boot-to-claude/kernel/game/game.c has been updated successfully. (file state is current in your context — no need to Read it back)
$ pkill -f 'qemu-system-i386.*4555' 2>/dev/null cd /home/martin/workspace/prototypes/boot-to-claude timeout 60 make 2>&1 | tail -15
Exit code 144
$ cd /home/martin/workspace/prototypes/boot-to-claude timeout 60 make 2>&1 | tail -15
build/toolchain/cc -Ikernel -Itoolchain/include -o build/game/game.S kernel/game/game.c build/toolchain/as -f elf32 -o build/game/game.o build/game/game.S build/toolchain/ld -T kernel/link.ld --format bin -o build/kernel.bin build/entry.o build/idt_stubs.o build/kmain.o build/console.o build/serial.o build/gdt.o build/idt.o build/string.o build/memory.o build/selftest.o build/ata.o build/fs/tinyfs.o build/fs/tinyfs_disk.o build/keyboard.o build/input.o build/shell/shell.o build/game/game.o build/lang/lexer.o build/lang/script/ast.o build/lang/script/parser.o build/lang/script/eval.o build/lang/compiler/parser.o build/lang/compiler/codegen.o build/lang/vm.o dd if=/dev/zero of=build/disk.img bs=512 count=4096 status=none dd if=build/boot.bin of=build/disk.img conv=notrunc status=none dd if=build/stage2.bin of=build/disk.img bs=512 seek=1 conv=notrunc status=none dd if=build/kernel.bin of=build/disk.img bs=512 seek=5 conv=notrunc status=none build/mkfs_tinyfs build/disk.img tests/hello.tsh:hello.tsh tests/hello.tc:hello.tc mkfs_tinyfs: done rm build/game/game.S
$ cd /home/martin/workspace/prototypes/boot-to-claude
qemu-system-i386 \
-drive file=build/disk.img,format=raw \
-display none \
-serial tcp:127.0.0.1:4556,server,nowait \
-no-rebootCommand running in background with ID: bu1jh0ir6. Output is being written to: /tmp/claude-1001/-home-martin-workspace-prototypes-boot-to-claude/03468d1c-5b29-4e24-8f0d-2a7435d227ab/tasks/bu1jh0ir6.output. You will be notified when it completes. To check interim output, use Read on that file path. Session cwd remains /home/martin/workspace/prototypes/boot-to-claude; directory changes made by the backgrounded command do not apply to subsequent commands.
$ sleep 3
python3 - <<'PYEOF'
import socket, time
s = socket.create_connection(("127.0.0.1", 4556), timeout=10)
s.settimeout(2)
def drain(t=2.0):
buf = b""
end = time.time() + t
while time.time() < end:
try:
chunk = s.recv(4096)
if not chunk:
break
buf += chunk
except socket.timeout:
break
return buf.decode(errors="replace")
def send(line):
s.sendall((line + "\n").encode())
return drain(2.0)
print(send("help"))
print("=== play ===")
print(send("play"))
print("=== choose starter 2 (Aqualing, water) ===")
print(send("2"))
# battle several turns with a mix of move choices and a run, to exercise both attacker/defender RNG paths broadly
for i in range(8):
out = send("1")
print(f"--- turn {i} ---")
print(out)
if "Game over" in out or "Champion" in out:
break
s.close()
PYEOFhelp
commands: ls, cat <file>, write <file> <text>, rm <file>, run <file>,
build <src> <out>, exec <file>, play, help, clear, reboot
>
=== play ===
play
=== Tiny Monsters ===
Defeat 5 wild monsters to become champion. Type 'quit' any time to leave.
Choose your starter:
1) Emberpup (Fire)
2) Aqualing (Water)
3) Leafling (Grass)
>
=== choose starter 2 (Aqualing, water) ===
2
=== A wild Leafling appears! (stage 1)
Your Aqualing: HP 30/30
Wild Leafling: HP 32/32
1) Water Jet (7)
2) Tail Slap (5)
3) Tide Wave (10)
4) Run
>
--- turn 0 ---
1
Your Aqualing uses Water Jet!
It's not very effective...
Wild Leafling takes 5 damage. (HP 27/32)
Wild Leafling uses Vine Whip!
It's super effective!
Your Aqualing takes 11 damage. (HP 19/30)
Your Aqualing: HP 19/30
Wild Leafling: HP 27/32
1) Water Jet (7)
2) Tail Slap (5)
3) Tide Wave (10)
4) Run
>
--- turn 1 ---
1
Your Aqualing uses Water Jet!
It's not very effective...
Wild Leafling takes 3 damage. (HP 24/32)
Wild Leafling uses Vine Whip!
It's super effective!
Your Aqualing takes 12 damage. (HP 7/30)
Your Aqualing: HP 7/30
Wild Leafling: HP 24/32
1) Water Jet (7)
2) Tail Slap (5)
3) Tide Wave (10)
4) Run
>
--- turn 2 ---
1
Your Aqualing uses Water Jet!
It's not very effective...
Wild Leafling takes 1 damage. (HP 23/32)
Wild Leafling uses Leaf Storm!
It's super effective!
Your Aqualing takes 13 damage. (HP 0/30)
Your Aqualing fainted... Game over! You reached stage 1.
>$ python3 - <<'PYEOF'
import socket, time
s = socket.create_connection(("127.0.0.1", 4556), timeout=10)
s.settimeout(2)
def drain(t=2.0):
buf = b""
end = time.time() + t
while time.time() < end:
try:
chunk = s.recv(4096)
if not chunk:
break
buf += chunk
except socket.timeout:
break
return buf.decode(errors="replace")
def send(line):
s.sendall((line + "\n").encode())
return drain(2.0)
print(send("play"))
print("=== starter 3 (Leafling, grass - beats Water, common wild) ===")
print(send("3"))
print("=== garbage input ===")
print(send("banana"))
print("=== run ===")
print(send("run"))
print("=== quit mid-shell-loop after run ===")
print(send("quit"))
print("=== back at shell? try help ===")
print(send("help"))
s.close()
PYEOFplay
=== Tiny Monsters ===
Defeat 5 wild monsters to become champion. Type 'quit' any time to leave.
Choose your starter:
1) Emberpup (Fire)
2) Aqualing (Water)
3) Leafling (Grass)
>
=== starter 3 (Leafling, grass - beats Water, common wild) ===
3
=== A wild Aqualing appears! (stage 1)
Your Leafling: HP 32/32
Wild Aqualing: HP 30/30
1) Vine Whip (7)
2) Tackle (5)
3) Leaf Storm (10)
4) Run
>
=== garbage input ===
banana
invalid choice - type 1-3 for a move, 'run', or 'quit'
Your Leafling: HP 32/32
Wild Aqualing: HP 30/30
1) Vine Whip (7)
2) Tackle (5)
3) Leaf Storm (10)
4) Run
>
=== run ===
run
You ran away!
=== A wild Leafling appears! (stage 1)
Your Leafling: HP 32/32
Wild Leafling: HP 32/32
1) Vine Whip (7)
2) Tackle (5)
3) Leaf Storm (10)
4) Run
>
=== quit mid-shell-loop after run ===
quit
Left the game.
>
=== back at shell? try help ===
help
commands: ls, cat <file>, write <file> <text>, rm <file>, run <file>,
build <src> <out>, exec <file>, play, help, clear, reboot
>$ pkill -f 'qemu-system-i386.*4556' 2>/dev/null cat /home/martin/workspace/prototypes/boot-to-claude/tools/run_tests.sh
Exit code 144
$ cat /home/martin/workspace/prototypes/boot-to-claude/tools/run_tests.sh
#!/usr/bin/env bash
# Boots the kernel headlessly in QEMU, checks the kernel self-test result,
# then drives the interactive shell via QEMU's monitor `sendkey` (see
# tools/qemu_type.py - there's no other way to feed the polled PS/2
# keyboard driver without real hardware) and asserts on serial output.
set -euo pipefail
cd "$(dirname "$0")/.."
make >/dev/null
BUILD=build
LOG="$BUILD/serial.log"
MON="$BUILD/qemu-monitor.sock"
rm -f "$LOG" "$MON"
qemu-system-i386 \
-drive file="$BUILD/disk.img",format=raw \
-display none \
-serial file:"$LOG" \
-monitor unix:"$MON",server,nowait \
-no-reboot &
QEMU_PID=$!
cleanup() {
kill "$QEMU_PID" 2>/dev/null || true
wait "$QEMU_PID" 2>/dev/null || true
}
trap cleanup EXIT
fail() {
echo "run_tests: FAIL - $1"
echo "--- serial log ---"
cat "$LOG" 2>/dev/null || true
exit 1
}
wait_for() {
local pattern="$1"
local max_tries="${2:-50}" # 50 * 0.2s = 10s
local i=0
while ! grep -qF "$pattern" "$LOG" 2>/dev/null; do
i=$((i + 1))
if [ "$i" -ge "$max_tries" ]; then
fail "timed out waiting for '$pattern'"
fi
sleep 0.2
done
}
type_line() {
python3 tools/qemu_type.py "$MON" "$1"$'\n'
}
# Checks that every non-blank line in tests/expected/<fixture> appears
# (in order) somewhere in the serial log - used for .tsh/.tc fixtures run
# through the shell's `run`/`build`+`exec` commands.
check_fixture_output() {
local expected_file="$1"
while IFS= read -r want; do
[ -z "$want" ] && continue
wait_for "$want" 30
done < "$expected_file"
}
for i in $(seq 1 25); do
[ -S "$MON" ] && break
sleep 0.2
done
[ -S "$MON" ] || fail "qemu monitor socket never appeared"
wait_for "SELFTEST PASS"
wait_for "type 'help'"
type_line "ls"
wait_for "hello.txt"
type_line "cat hello.txt"
wait_for "Hello, tinyfs!"
type_line "write greet.txt hi there"
wait_for "write: ok"
type_line "cat greet.txt"
wait_for "hi there"
type_line "rm greet.txt"
wait_for "rm: ok"
type_line "cat greet.txt"
wait_for "cat: no such file"
type_line "help"
wait_for "commands:"
type_line "run hello.tsh"
check_fixture_output tests/expected/hello.tsh.txt
type_line "build hello.tc hello.tbc"
wait_for "build: ok"
type_line "exec hello.tbc"
check_fixture_output tests/expected/hello.tc.txt
echo "run_tests: PASS"- type_line "build hello.tc hello.tbc" - wait_for "build: ok" - - type_line "exec hello.tbc" - check_fixture_output tests/expected/hello.tc.txt - - echo "run_tests: PASS" + type_line "build hello.tc hello.tbc" + wait_for "build: ok" + + type_line "exec hello.tbc" + check_fixture_output tests/expected/hello.tc.txt + + # kernel/game: battle outcomes are RNG-dependent, so this only asserts on + # the deterministic parts of the flow (menu structure, run/quit + # mechanics) - see tools/run_serial_tests.sh for a full battle played to + # an end state, driven entirely over the serial port instead of the + # keyboard (the scenario this feature exists for - see docs/game.md). + type_line "play" + wait_for "Choose your starter:" + type_line "1" + wait_for "4) Run" + type_line "run" + wait_for "You ran away!" + type_line "quit" + wait_for "Left the game." + + echo "run_tests: PASS"
The file /home/martin/workspace/prototypes/boot-to-claude/tools/run_tests.sh has been updated successfully. (file state is current in your context — no need to Read it back)
#!/usr/bin/env python3
"""Drives kernel/game/game.c entirely over the serial port - no PS/2
keyboard, no QEMU monitor `sendkey` injection (contrast tools/qemu_type.py,
used by tools/run_tests.sh for the keyboard path). This is the actual
scenario kernel/input.c's dual keyboard+serial polling exists for: an
external agent (an AI agent, or this script standing in for one) that
only has a serial connection to the guest, sending commands and reading
responses as plain text.
Connects to the QEMU `-serial unix:PATH,server,nowait` socket tools/
run_serial_tests.sh boots the guest with. Battle outcomes are
RNG-dependent (see game.c's rng_next), so this plays real turns to a
real end state (a faint or a win) rather than asserting on a specific
outcome - it accepts either.
"""
import socket
import sys
import time
TIMEOUT_S = 20.0
POLL_S = 0.1
def connect(sock_path):
for _ in range(50): # 50 * 0.2s = 10s
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(sock_path)
s.settimeout(POLL_S)
return s
except (FileNotFoundError, ConnectionRefusedError):
time.sleep(0.2)
raise SystemExit(f"serial_game_test: socket {sock_path} never appeared")
class Session:
def __init__(self, sock):
self.sock = sock
self.buf = ""
def send(self, line):
self.sock.sendall((line + "\n").encode())
def wait_for(self, pattern, timeout=TIMEOUT_S):
deadline = time.time() + timeout
while pattern not in self.buf:
if time.time() > deadline:
raise SystemExit(
f"serial_game_test: timed out waiting for {pattern!r}\n"
f"--- received so far ---\n{self.buf}"
)
try:
chunk = self.sock.recv(4096)
except socket.timeout:
continue
if not chunk:
raise SystemExit("serial_game_test: connection closed unexpectedly")
self.buf += chunk.decode(errors="replace")
# Trim everything up to and including the match, so the next
# wait_for() only sees genuinely new output.
self.buf = self.buf[self.buf.index(pattern) + len(pattern):]
def wait_for_any(self, patterns, timeout=TIMEOUT_S):
deadline = time.time() + timeout
while True:
for p in patterns:
if p in self.buf:
self.buf = self.buf[self.buf.index(p) + len(p):]
return p
if time.time() > deadline:
raise SystemExit(
f"serial_game_test: timed out waiting for any of {patterns!r}\n"
f"--- received so far ---\n{self.buf}"
)
try:
chunk = self.sock.recv(4096)
except socket.timeout:
continue
if not chunk:
raise SystemExit("serial_game_test: connection closed unexpectedly")
self.buf += chunk.decode(errors="replace")
def main():
if len(sys.argv) != 2:
print(f"usage: {sys.argv[0]} <serial-unix-socket>", file=sys.stderr)
sys.exit(1)
sess = Session(connect(sys.argv[1]))
sess.wait_for("SELFTEST PASS")
sess.wait_for("type 'help'")
sess.send("help")
sess.wait_for("commands:")
print("ok: shell reachable over serial (no keyboard involved)")
sess.send("play")
sess.wait_for("Choose your starter:")
sess.send("1")
sess.wait_for("4) Run")
print("ok: game reachable, starter menu -> battle screen")
# Play real turns (move 1 each time) to a real end state - either
# outcome is fine, this is just proving serial-only input drives a
# full game to completion, not asserting a specific RNG result.
outcome = None
for _ in range(40):
sess.send("1")
outcome = sess.wait_for_any(["Game over!", "you are the Champion!", "4) Run"])
if outcome != "4) Run":
break
if outcome == "4) Run":
raise SystemExit("serial_game_test: battle never reached an end state")
print(f"ok: battle played to completion over serial ({outcome.strip()})")
sess.send("help")
sess.wait_for("commands:")
print("ok: shell still responsive after the game returned")
print("serial_game_test: PASS")
if __name__ == "__main__":
main()File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tools/serial_game_test.py (file state is current in your context — no need to Read it back)
#!/usr/bin/env bash
# Boots the kernel headlessly and drives kernel/game/game.c entirely over
# the serial port via tools/serial_game_test.py - no PS/2 keyboard, no
# QEMU monitor `sendkey` (contrast tools/run_tests.sh, which uses the
# keyboard path). This is the actual scenario kernel/input.c's dual
# keyboard+serial polling exists for: an external agent connected only
# over serial. See docs/game.md.
set -euo pipefail
cd "$(dirname "$0")/.."
make >/dev/null
BUILD=build
SOCK="$BUILD/qemu-serial.sock"
rm -f "$SOCK"
qemu-system-i386 \
-drive file="$BUILD/disk.img",format=raw \
-display none \
-serial unix:"$SOCK",server,nowait \
-no-reboot &
QEMU_PID=$!
cleanup() {
kill "$QEMU_PID" 2>/dev/null || true
wait "$QEMU_PID" 2>/dev/null || true
}
trap cleanup EXIT
python3 tools/serial_game_test.py "$SOCK"
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/tools/run_serial_tests.sh (file state is current in your context — no need to Read it back)
- outcome = None - for _ in range(40): + outcome = "4) Run" # sentinel: "no end state reached yet" + for _ in range(40):
The file /home/martin/workspace/prototypes/boot-to-claude/tools/serial_game_test.py has been updated successfully. (file state is current in your context — no need to Read it back)
$ chmod +x /home/martin/workspace/prototypes/boot-to-claude/tools/run_serial_tests.sh /home/martin/workspace/prototypes/boot-to-claude/tools/serial_game_test.py
(Bash completed with no output)
$ cd /home/martin/workspace/prototypes/boot-to-claude timeout 60 ./tools/run_tests.sh 2>&1 | tail -20
run_tests: PASS qemu-system-i386: terminating on signal 15 from pid 1697603 (bash)
$ cd /home/martin/workspace/prototypes/boot-to-claude timeout 60 ./tools/run_serial_tests.sh 2>&1 | tail -30
ok: shell reachable over serial (no keyboard involved) ok: game reachable, starter menu -> battle screen ok: battle played to completion over serial (Game over!) ok: shell still responsive after the game returned serial_game_test: PASS qemu-system-i386: terminating on signal 15 from pid 1697667 (bash)
$ cd /home/martin/workspace/prototypes/boot-to-claude for i in 1 2 3; do echo "=== run $i ===" timeout 60 ./tools/run_serial_tests.sh 2>&1 | grep -E 'ok:|PASS|FAIL|Error' done
=== run 1 === ok: shell reachable over serial (no keyboard involved) ok: game reachable, starter menu -> battle screen ok: battle played to completion over serial (Game over!) ok: shell still responsive after the game returned serial_game_test: PASS === run 2 === ok: shell reachable over serial (no keyboard involved) ok: game reachable, starter menu -> battle screen ok: battle played to completion over serial (Game over!) ok: shell still responsive after the game returned serial_game_test: PASS === run 3 === ok: shell reachable over serial (no keyboard involved) ok: game reachable, starter menu -> battle screen ok: battle played to completion over serial (Game over!) ok: shell still responsive after the game returned serial_game_test: PASS
$ cd /home/martin/workspace/prototypes/boot-to-claude make clean >/dev/null timeout 300 make 2>&1 | tail -10 echo "=== make exit=$? ===" timeout 60 ./tools/run_tests.sh 2>&1 | tail -5 timeout 60 ./tools/run_serial_tests.sh 2>&1 | tail -10
build/toolchain/cc --target=host -Ikernel -Itools -Itoolchain/include -o build/host_mkfs_tinyfs.S tools/mkfs_tinyfs.c build/toolchain/as -f elf32 -o build/host_mkfs_tinyfs.o build/host_mkfs_tinyfs.S build/toolchain/ld -o build/mkfs_tinyfs build/host_tinyfs.o build/host_string.o build/host_mkfs_tinyfs.o dd if=/dev/zero of=build/disk.img bs=512 count=4096 status=none dd if=build/boot.bin of=build/disk.img conv=notrunc status=none dd if=build/stage2.bin of=build/disk.img bs=512 seek=1 conv=notrunc status=none dd if=build/kernel.bin of=build/disk.img bs=512 seek=5 conv=notrunc status=none build/mkfs_tinyfs build/disk.img tests/hello.tsh:hello.tsh tests/hello.tc:hello.tc mkfs_tinyfs: done rm build/console.S build/shell/shell.S build/idt.S build/lang/script/parser.S build/lang/script/ast.S build/lang/script/eval.S build/lang/vm.S build/lang/compiler/codegen.S build/lang/compiler/parser.S build/kmain.S build/gdt.S build/string.S build/fs/tinyfs.S build/selftest.S build/serial.S build/lang/lexer.S build/fs/tinyfs_disk.S build/ata.S build/input.S build/game/game.S build/keyboard.S build/memory.S === make exit=0 === run_tests: PASS qemu-system-i386: terminating on signal 15 from pid 1698054 (bash) ok: shell reachable over serial (no keyboard involved) ok: game reachable, starter menu -> battle screen ok: battle played to completion over serial (Game over!) ok: shell still responsive after the game returned serial_game_test: PASS qemu-system-i386: terminating on signal 15 from pid 1698136 (bash)
# Tiny Monsters (`kernel/game/`)
A small turn-based, Pokemon-like battle game built into the kernel,
reachable from the shell's `play` command. It exists specifically to be
playable by an external agent (an AI agent, or any other program) that
only has a serial connection to the machine - no keyboard, no display.
## Dual input: keyboard *or* serial
Every other input path in this kernel (the shell) used to read
exclusively from the polled PS/2 keyboard (`kernel/keyboard.c`). Serial
(`kernel/serial.c`, COM1) was write-only - used for the existing
dual-output logging (`console_write` + `serial_write` side by side
everywhere), never for input.
`kernel/input.c` adds that: `serial_has_char()`/`serial_getc()` (serial
RX) and `keyboard_has_char()` (a non-blocking peek at the PS/2
controller) let `input_getchar()` poll both peripherals and return
whichever produces a character first. Both the shell (`kernel/shell/
shell.c`) and the game (`kernel/game/game.c`) read all their input
through `input_getchar()`, so a human at the keyboard and an agent
connected over serial can drive either one interchangeably, in the same
session, with no configuration - matching the existing dual-*output*
convention rather than adding a new, separate input mode.
## Playing over serial
Boot with a bidirectional serial backend instead of (or in addition to)
a display - e.g.:
```
qemu-system-i386 -drive file=build/disk.img,format=raw \
-display none -serial unix:/tmp/tinyos-serial.sock,server,nowait
```
Then connect to that socket and treat it as a plain line-oriented text
stream: read until `SELFTEST PASS` and the shell's `> ` prompt, send
`play\n`, read the starter menu, send a digit, read the battle screen,
and so on - exactly what `tools/serial_game_test.py` does (see
Testing below). No special protocol; it's the same text a human would
see and type at a terminal.
## Rules
- **Starters**: three species, one of each type - `Emberpup` (Fire),
`Aqualing` (Water), `Leafling` (Grass). The wild encounter pool draws
from the same three.
- **Types**: a simple rock-paper-scissors cycle, Fire beats Grass beats
Water beats Fire. A type-advantaged move does 1.5x damage ("It's super
effective!"); a disadvantaged one does 0.5x ("It's not very
effective..."); same/no relationship is 1x.
- **Battle**: the player always attacks first each turn; if the wild
monster survives, it counterattacks with a random one of its own three
moves. Damage is `move power`, type-adjusted, `+/-2` random variance,
minimum 1.
- **Progression**: HP does *not* regenerate between encounters - only
fainting ends a run. Each wild monster defeated increases `stage`
(their HP scales up slightly with it); reach stage 6 (5 wins) to become
Champion. Your own monster fainting ends the run immediately.
- **Commands** at any prompt: a digit picks a starter or a move; `run`
flees the current encounter (a fresh one follows, same stage); `quit`
leaves the game and returns to the shell immediately.
## Testing
Two complementary automated tests, both driving a full session against
a headless QEMU instance and asserting on the resulting output - see
each script's own header comment for the exact mechanism:
- `tools/run_tests.sh` - the shell's existing keyboard-driven suite,
extended with a `play` pass that checks the deterministic parts of the
flow (menu structure, `run`/`quit`) via QEMU monitor `sendkey`
injection (`tools/qemu_type.py`), the same way every other shell
command is tested.
- `tools/run_serial_tests.sh` (+ `tools/serial_game_test.py`) - the
scenario this feature actually exists for: no keyboard input at all,
driven entirely over a `-serial unix:...` socket. Plays a real battle
to a real end state (a faint or a win - battle outcomes are
RNG-dependent, so this accepts either) to prove the whole shell+game
loop works end to end over serial alone, then confirms the shell is
still responsive afterward.
File created successfully at: /home/martin/workspace/prototypes/boot-to-claude/docs/game.md (file state is current in your context — no need to Read it back)
55 `gcc` by default. That bootstrap step never runs again after the first
56 `build/toolchain/{as,cc,ld}` exist: `nasm`/`ld`/`objcopy` aren't needed
57 at all, and `gcc` never touches OS code, only `toolchain/`'s own source.
58 No cross-compiler needed.
59
60 ## Architecture
61
62 ```
63 BIOS -> boot/boot.S (MBR, 512B) real mode
64 -> boot/stage2.S real mode -> protected mode
65 -> kernel/entry.S -> kmain.c 32-bit protected mode, flat, no paging
66 gdt.c / idt.c kernel-owned GDT; IDT covers CPU
67 exceptions only - no PIC/IRQs, ever
68 console.c / serial.c VGA text + COM1, dual-output throughout
69 memory.c free-list kmalloc/kfree over a 1MB heap
70 ata.c -> fs/tinyfs.c polled ATA PIO -> tinyfs filesystem
71 keyboard.c -> shell/shell.c polled PS/2 -> command REPL
72 lang/lexer.c (shared)
73 -> lang/script/{parser,ast,eval}.c scripting language (tree-walking)
74 -> lang/compiler/{parser,codegen}.c compiler -> lang/vm.c (bytecode VM)
75 ```
76
77 Deliberately single-tasking: no scheduler, no preemption, no interrupts
78 enabled after boot, no process/syscall boundary. The shell and both
79 language runtimes execute directly in kernel context. This keeps the OS
80 layer small so the interesting work is in the filesystem and language
81 implementations - see [`docs/`](docs/) for their designs:
82
83 - [`docs/tinyfs.md`](docs/tinyfs.md) - on-disk filesystem format
84 - [`docs/scripting-language.md`](docs/scripting-language.md) - the `tsh` language
85 - [`docs/bytecode-isa.md`](docs/bytecode-isa.md) - the compiled language + VM ISA
86 - [`docs/toolchain.md`](docs/toolchain.md) - the self-hosted assembler/compiler/linker
87
88 ## Repo layout
89
90 ```
91 boot/ stage1 (MBR) + stage2 boot loaders
92 kernel/ the kernel itself (see architecture diagram above)
93 kernel/fs/ tinyfs (shared: freestanding in the kernel, native in mkfs_tinyfs)
94 kernel/lang/ shared lexer + the two language implementations
95 kernel/shell/ interactive command REPL
96 toolchain/ self-hosted AS/CC/LD (see docs/toolchain.md) - host-side
97 build infrastructure, not OS code, but self-authored
98 like everything else in this repo
99 tools/ host-side build/test infrastructure (not OS code):
100 mkfs_tinyfs.c formats + seeds build/disk.img at build time
101 bootstrap_selfhost.sh builds+verifies toolchain/ (Stage1/2/3)
102 run_tests.sh headless QEMU test harness
103 run_toolchain_tests.sh host-side AS/CC/LD test harness
104 qemu_type.py scripts keystrokes into QEMU via its HMP monitor- console.c / serial.c VGA text + COM1, dual-output throughout
- memory.c free-list kmalloc/kfree over a 1MB heap
- ata.c -> fs/tinyfs.c polled ATA PIO -> tinyfs filesystem
- keyboard.c -> shell/shell.c polled PS/2 -> command REPL
- lang/lexer.c (shared)
- -> lang/script/{parser,ast,eval}.c scripting language (tree-walking)
- -> lang/compiler/{parser,codegen}.c compiler -> lang/vm.c (bytecode VM)
- ```
+ console.c / serial.c VGA text + COM1, dual-output throughout
+ memory.c free-list kmalloc/kfree over a 1MB heap
+ ata.c -> fs/tinyfs.c polled ATA PIO -> tinyfs filesystem
+ keyboard.c + input.c -> shell/shell.c PS/2 or serial RX -> command REPL
+ lang/lexer.c (shared)
+ -> lang/script/{parser,ast,eval}.c scripting language (tree-walking)
+ -> lang/compiler/{parser,codegen}.c compiler -> lang/vm.c (bytecode VM)
+ game/game.c shell's `play` - see docs/game.md
+ ```The file /home/martin/workspace/prototypes/boot-to-claude/README.md has been updated successfully. (file state is current in your context — no need to Read it back)
- - [`docs/tinyfs.md`](docs/tinyfs.md) - on-disk filesystem format - - [`docs/scripting-language.md`](docs/scripting-language.md) - the `tsh` language - - [`docs/bytecode-isa.md`](docs/bytecode-isa.md) - the compiled language + VM ISA - - [`docs/toolchain.md`](docs/toolchain.md) - the self-hosted assembler/compiler/linker + - [`docs/tinyfs.md`](docs/tinyfs.md) - on-disk filesystem format + - [`docs/scripting-language.md`](docs/scripting-language.md) - the `tsh` language + - [`docs/bytecode-isa.md`](docs/bytecode-isa.md) - the compiled language + VM ISA + - [`docs/toolchain.md`](docs/toolchain.md) - the self-hosted assembler/compiler/linker + - [`docs/game.md`](docs/game.md) - the `play` battle game, playable over serial only
The file /home/martin/workspace/prototypes/boot-to-claude/README.md has been updated successfully. (file state is current in your context — no need to Read it back)
98 toolchain/ self-hosted AS/CC/LD (see docs/toolchain.md) - host-side 99 build infrastructure, not OS code, but self-authored 100 like everything else in this repo 101 tools/ host-side build/test infrastructure (not OS code): 102 mkfs_tinyfs.c formats + seeds build/disk.img at build time 103 bootstrap_selfhost.sh builds+verifies toolchain/ (Stage1/2/3) 104 run_tests.sh headless QEMU test harness 105 run_toolchain_tests.sh host-side AS/CC/LD test harness 106 qemu_type.py scripts keystrokes into QEMU via its HMP monitor 107 tests/ fixtures (*.tsh, *.tc) + tests/expected/ serial-output fixtures 108 tests/toolchain/ AS/CC/LD test fixtures (see run_toolchain_tests.sh) 109 docs/ design docs, listed above 110 ``` 111 112 ## Testing 113 114 There's no hardware and no display in CI, so the serial port is the test 115 oracle throughout: `kernel/selftest.c` runs a boot-time self-test (string 116 ops, the allocator including coalescing, and a tinyfs create/write/read/ 117 delete roundtrip) and prints `SELFTEST PASS`/`FAIL`. 118 119 ``` 120 tools/run_tests.sh 121 ``` 122 123 boots `build/disk.img` headless (`-display none -serial file:...`), waits 124 for `SELFTEST PASS`, then drives the shell by scripting keystrokes into 125 QEMU's HMP monitor over a unix socket (`tools/qemu_type.py sendkey ...`) 126 - the only way to feed the kernel's polled PS/2 driver without real 127 hardware - and asserts on the resulting serial output for every shell
127 hardware - and asserts on the resulting serial output for every shell 128 command, `run hello.tsh`, and `build`+`exec` on `hello.tc`. 129 130 ## Boot budget 131 132 The kernel image is capped at a fixed number of disk sectors (`boot/ 133 stage2.S`'s `KERNEL_SECTORS`, currently 512 = 256KB), read via BIOS 134 INT13h in bounded chunks (SeaBIOS silently misbehaves on very large 135 single extended-read requests). `kernel/fs/tinyfs.h`'s 136 `TINYFS_FS_START_LBA` reserves exactly that much room before the 137 filesystem region begins. The Makefile fails the build loudly if the 138 kernel image ever grows past its budget - if you hit that, bump 139 `KERNEL_SECTORS`, the Makefile's `KERNEL_MAX_SECTORS`, and 140 `TINYFS_FS_START_LBA` together. 141
- tools/ host-side build/test infrastructure (not OS code): - mkfs_tinyfs.c formats + seeds build/disk.img at build time - bootstrap_selfhost.sh builds+verifies toolchain/ (Stage1/2/3) - run_tests.sh headless QEMU test harness - run_toolchain_tests.sh host-side AS/CC/LD test harness - qemu_type.py scripts keystrokes into QEMU via its HMP monitor + tools/ host-side build/test infrastructure (not OS code): + mkfs_tinyfs.c formats + seeds build/disk.img at build time + bootstrap_selfhost.sh builds+verifies toolchain/ (Stage1/2/3) + run_tests.sh headless QEMU test harness (keyboard-driven) + run_serial_tests.sh same, but driven entirely over serial (see docs/game.md) + run_toolchain_tests.sh host-side AS/CC/LD test harness + qemu_type.py scripts keystrokes into QEMU via its HMP monitor + serial_game_test.py drives kernel/game/ over a serial socket, no keyboard
The file /home/martin/workspace/prototypes/boot-to-claude/README.md has been updated successfully. (file state is current in your context — no need to Read it back)
- hardware - and asserts on the resulting serial output for every shell - command, `run hello.tsh`, and `build`+`exec` on `hello.tc`. + hardware - and asserts on the resulting serial output for every shell + command, `run hello.tsh`, `build`+`exec` on `hello.tc`, and the `play` + battle game's menu structure. + + ``` + tools/run_serial_tests.sh + ``` + + the same idea, but for `kernel/input.c`'s *other* input path: no + keyboard/HMP injection at all, driven entirely over a second, genuinely + bidirectional serial connection (`-serial unix:...,server,nowait`) via + `tools/serial_game_test.py` - the actual scenario that path exists for + (an external agent with only a serial connection). See + [`docs/game.md`](docs/game.md).
The file /home/martin/workspace/prototypes/boot-to-claude/README.md has been updated successfully. (file state is current in your context — no need to Read it back)
$ cd /home/martin/workspace/prototypes/boot-to-claude git status --short
M Makefile M README.md M kernel/keyboard.c M kernel/keyboard.h M kernel/serial.c M kernel/serial.h M kernel/shell/shell.c M tools/run_tests.sh ?? docs/game.md ?? kernel/game/ ?? kernel/input.c ?? kernel/input.h ?? tools/run_serial_tests.sh ?? tools/serial_game_test.py
$ cd /home/martin/workspace/prototypes/boot-to-claude make clean >/dev/null timeout 300 make >/dev/null 2>&1 echo "make exit=$?" timeout 60 ./tools/run_tests.sh 2>&1 | tail -5 timeout 60 ./tools/run_serial_tests.sh 2>&1 | tail -10 timeout 90 ./tools/run_toolchain_tests.sh 2>&1 | tail -5
make exit=0 run_tests: PASS qemu-system-i386: terminating on signal 15 from pid 1698603 (bash) ok: shell reachable over serial (no keyboard involved) ok: game reachable, starter menu -> battle screen ok: battle played to completion over serial (Game over!) ok: shell still responsive after the game returned serial_game_test: PASS qemu-system-i386: terminating on signal 15 from pid 1698662 (bash) PASS t12_local_array_size (4) PASS t13_struct_copy (212) PASS t14_struct_return (44) PASS t15_escapes (158) run_toolchain_tests: PASS
$ cd /home/martin/workspace/prototypes/boot-to-claude git add Makefile README.md kernel/keyboard.c kernel/keyboard.h kernel/serial.c kernel/serial.h \ kernel/shell/shell.c tools/run_tests.sh docs/game.md kernel/game/ kernel/input.c kernel/input.h \ tools/run_serial_tests.sh tools/serial_game_test.py git status --short
M Makefile M README.md A docs/game.md A kernel/game/game.c A kernel/game/game.h A kernel/input.c A kernel/input.h M kernel/keyboard.c M kernel/keyboard.h M kernel/serial.c M kernel/serial.h M kernel/shell/shell.c A tools/run_serial_tests.sh M tools/run_tests.sh A tools/serial_game_test.py