Claude Code transcript - page 1/1

Implement the following plan:

Plan: Raw terminal mode for keyboard channel (Issue #90)

Context

The generated entry harness reads keyboard input via bufio.NewReader(os.Stdin), which operates in the terminal's default line-buffered mode. Characters aren't available until Enter is pressed, breaking programs like echoing.occ that expect character-by-character input. We need to put the terminal in raw mode so bytes are available immediately as typed.

Approach

Use golang.org/x/term to switch stdin to raw mode in the generated entry harness. Fall back to the current buffered behavior when stdin is not a terminal (piped input).

Changes

1. Add dependency

  • go get golang.org/x/term — adds the package to go.mod/go.sum

2. codegen/codegen.go — Generator struct (line ~21)

  • Add needTerm bool flag

3. codegen/codegen.go — flag initialization (line ~106)

  • Add g.needTerm = false

4. codegen/codegen.go — entry harness detection (lines 216-220)

  • Set g.needTerm = true when entry harness is detected

5. codegen/codegen.go — import generation (lines 228-256)

  • Add condition for needTerm in the import block guard
  • When needTerm, emit imports for "os/signal", "syscall", and "golang.org/x/term" (alphabetical order within the block)

6. codegen/codegen.gogenerateEntryHarness() (lines 419-512)

Rewrite to generate code structured like this:

func main() {
    keyboard := make(chan byte, 256)
    screen := make(chan byte, 256)
    _error := make(chan byte, 256)

    // Raw terminal mode
    var rawMode bool
    var oldState *term.State
    fd := int(os.Stdin.Fd())
    if term.IsTerminal(fd) {
        var err error
        oldState, err = term.MakeRaw(fd)
        if err == nil {
            rawMode = true
            defer term.Restore(fd, oldState)
            // Restore terminal on signals (SIGINT won't fire from Ctrl+C in
            // raw mode, but can arrive via external kill; SIGTERM from kill)
            sigCh := make(chan os.Signal, 1)
            signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
            go func() {
                <-sigCh
                term.Restore(fd, oldState)
                os.Exit(1)
            }()
        }
    }

    var wg sync.WaitGroup
    wg.Add(2)

    // Screen writer — in raw mode, insert CR before LF
    go func() {
        defer wg.Done()
        w := bufio.NewWriter(os.Stdout)
        for b := range screen {
            if b == 255 {
                w.Flush()
            } else {
                if rawMode && b == '\n' {
                    w.WriteByte('\r')
                }
                w.WriteByte(b)
            }
        }
        w.Flush()
    }()

    // Error writer — same CR/LF handling
    go func() { /* same pattern as screen, writing to os.Stderr */ }()

    // Keyboard reader
    go func() {
        if rawMode {
            buf := make([]byte, 1)
            for {
                n, err := os.Stdin.Read(buf)
                if err != nil || n == 0 {
                    close(keyboard)
                    return
                }
                if buf[0] == 3 { // Ctrl+C
                    term.Restore(fd, oldState)
                    os.Exit(1)
                }
                keyboard <- buf[0]
            }
        } else {
            r := bufio.NewReader(os.Stdin)
            for {
                b, err := r.ReadByte()
                if err != nil {
                    close(keyboard)
                    return
                }
                keyboard <- b
            }
        }
    }()

    entryProc(keyboard, screen, _error)

    close(screen)
    close(_error)
    wg.Wait()
}

Key design decisions: - Ctrl+C handling: In raw mode, ISIG is disabled so Ctrl+C sends byte 3 instead of SIGINT. The keyboard reader intercepts byte 3 to restore terminal and exit cleanly. - LF→CRLF in output: Raw mode disables OPOST, so \n no longer auto-translates to \r\n. The screen/error writers insert CR before LF. This is harmless if the occam program already sends both *c and *n (the extra CR is a no-op). - Non-terminal fallback: When stdin is piped, skip raw mode and use the existing bufio.NewReader approach for efficient buffered reading. - Signal handler: Catches external SIGINT/SIGTERM to restore terminal state before exit.

7. codegen/codegen_test.go — unit test

  • Add test that verifies entry-harness-style program generates code containing term.MakeRaw, term.IsTerminal, and term.Restore.

8. codegen/e2e_helpers_test.go — new helper + e2e test

  • Add transpileCompileRunWithInput() helper that:
  • Initializes a Go module in the temp dir (go mod init test && go mod tidy)
  • Pipes stdin to the binary
  • Add e2e test: transpile echoing.occ-style program, pipe input "hello Z", verify output matches.

Verification

  1. go test ./... — all existing tests pass
  2. Manual test with echoing.occ: bash go build -o occam2go . && ./occam2go -I kroc/modules/course/libsrc -o /tmp/echo.go kroc/modules/course/examples/echoing.occ cd /tmp && go mod init echo_test && go mod tidy && go run echo.go # Type characters — they should echo immediately, one at a time # Type 'Z' to exit

If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: /home/david/.claude/projects/-home-david-projects-code-associates-occam2go/0114918c-281e-45dd-98ac-dfc9baf171b5.jsonl

Thinking

Let me start by understanding the current codebase state, then implement the plan step by step. Let me read the key files first.

I'll start by reading the key files I need to modify, then implement the changes.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go"
}
File content (32660 tokens) exceeds maximum allowed tokens (25000). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen_test.go"
}
<tool_use_error>Sibling tool call errored</tool_use_error>
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_test.go"
}
<tool_use_error>Sibling tool call errored</tool_use_error>
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/go.mod"
}
<tool_use_error>Sibling tool call errored</tool_use_error>

Let me read the files in smaller chunks.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "limit": 100
}
     1→package codegen
     2→
     3→import (
     4→	"fmt"
     5→	"strings"
     6→
     7→	"github.com/codeassociates/occam2go/ast"
     8→)
     9→
    10→// Generator converts an AST to Go code
    11→type Generator struct {
    12→	indent   int
    13→	builder  strings.Builder
    14→	needSync bool // track if we need sync package import
    15→	needFmt  bool // track if we need fmt package import
    16→	needTime bool // track if we need time package import
    17→	needOs   bool // track if we need os package import
    18→	needMath bool // track if we need math package import
    19→	needMathBits bool // track if we need math/bits package import
    20→	needBufio    bool // track if we need bufio package import
    21→	needReflect    bool // track if we need reflect package import
    22→	needBoolHelper bool // track if we need _boolToInt helper
    23→
    24→	// Track procedure signatures for proper pointer handling
    25→	procSigs map[string][]ast.ProcParam
    26→	// Track current procedure's reference parameters
    27→	refParams map[string]bool
    28→
    29→	// Protocol support
    30→	protocolDefs  map[string]*ast.ProtocolDecl
    31→	chanProtocols map[string]string // channel name → protocol name
    32→	tmpCounter    int               // for unique temp variable names
    33→
    34→	// Record support
    35→	recordDefs map[string]*ast.RecordDecl
    36→	recordVars map[string]string // variable name → record type name
    37→
    38→	// Channel element type tracking (for ALT guard codegen)
    39→	chanElemTypes map[string]string // channel name → Go element type
    40→
    41→	// Bool variable tracking (for type conversion codegen)
    42→	boolVars map[string]bool
    43→
    44→	// Nesting level: 0 = package level, >0 = inside a function
    45→	nestingLevel int
    46→
    47→	// RETYPES parameter renames: when a RETYPES declaration shadows a
    48→	// parameter (e.g. VAL INT X RETYPES X :), the parameter is renamed
    49→	// in the signature so := can create a new variable with the original name.
    50→	retypesRenames map[string]string
    51→}
    52→
    53→// Transputer intrinsic function names
    54→var transpIntrinsics = map[string]bool{
    55→	"LONGPROD":   true,
    56→	"LONGDIV":    true,
    57→	"LONGSUM":    true,
    58→	"LONGDIFF":   true,
    59→	"NORMALISE":  true,
    60→	"SHIFTRIGHT": true,
    61→	"SHIFTLEFT":  true,
    62→}
    63→
    64→// Built-in print procedures
    65→var printBuiltins = map[string]bool{
    66→	"print.int":     true,
    67→	"print.string":  true,
    68→	"print.bool":    true,
    69→	"print.newline": true,
    70→}
    71→
    72→// New creates a new code generator
    73→func New() *Generator {
    74→	return &Generator{}
    75→}
    76→
    77→// goIdent converts an occam identifier to a valid Go identifier.
    78→// Occam allows dots in identifiers (e.g., out.repeat); Go does not.
    79→// goReserved is a set of Go keywords and predeclared identifiers that cannot be
    80→// used as variable names when they also appear as type conversions in the generated code.
    81→var goReserved = map[string]bool{
    82→	"byte": true, "int": true, "string": true, "len": true, "cap": true,
    83→	"make": true, "new": true, "copy": true, "close": true, "delete": true,
    84→	"panic": true, "recover": true, "print": true, "println": true,
    85→	"error": true, "rune": true, "bool": true, "true": true, "false": true,
    86→}
    87→
    88→func goIdent(name string) string {
    89→	name = strings.ReplaceAll(name, ".", "_")
    90→	if goReserved[name] {
    91→		return "_" + name
    92→	}
    93→	return name
    94→}
    95→
    96→// Generate produces Go code from the AST
    97→func (g *Generator) Generate(program *ast.Program) string {
    98→	g.builder.Reset()
    99→	g.needSync = false
   100→	g.needFmt = false
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/go.mod"
}
     1→module github.com/codeassociates/occam2go
     2→
     3→go 1.25.6
     4→
Glob
{
  "pattern": "codegen/e2e_helpers_test.go",
  "path": "/home/david/projects/code-associates/occam2go"
}
/home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 100,
  "limit": 200
}
   100→	g.needFmt = false
   101→	g.needTime = false
   102→	g.needOs = false
   103→	g.needMath = false
   104→	g.needMathBits = false
   105→	g.needBufio = false
   106→	g.needReflect = false
   107→	g.needBoolHelper = false
   108→	g.procSigs = make(map[string][]ast.ProcParam)
   109→	g.refParams = make(map[string]bool)
   110→	g.protocolDefs = make(map[string]*ast.ProtocolDecl)
   111→	g.chanProtocols = make(map[string]string)
   112→	g.chanElemTypes = make(map[string]string)
   113→	g.tmpCounter = 0
   114→	g.recordDefs = make(map[string]*ast.RecordDecl)
   115→	g.recordVars = make(map[string]string)
   116→	g.boolVars = make(map[string]bool)
   117→
   118→	// Pre-pass: collect BOOL variable names (needed before containsBoolConversion)
   119→	for _, stmt := range program.Statements {
   120→		g.collectBoolVars(stmt)
   121→	}
   122→
   123→	// First pass: collect procedure signatures, protocols, and check for PAR/print
   124→	for _, stmt := range program.Statements {
   125→		if g.containsPar(stmt) {
   126→			g.needSync = true
   127→		}
   128→		if g.containsPrint(stmt) {
   129→			g.needFmt = true
   130→		}
   131→		if g.containsTimer(stmt) {
   132→			g.needTime = true
   133→		}
   134→		if g.containsStop(stmt) {
   135→			g.needOs = true
   136→			g.needFmt = true
   137→		}
   138→		if g.containsMostExpr(stmt) {
   139→			g.needMath = true
   140→		}
   141→		if g.containsIntrinsics(stmt) {
   142→			g.needMathBits = true
   143→		}
   144→		if g.containsRetypes(stmt) {
   145→			g.needMath = true
   146→		}
   147→		if g.containsAltReplicator(stmt) {
   148→			g.needReflect = true
   149→		}
   150→		if g.containsBoolConversion(stmt) {
   151→			g.needBoolHelper = true
   152→		}
   153→		if proc, ok := stmt.(*ast.ProcDecl); ok {
   154→			g.procSigs[proc.Name] = proc.Params
   155→			g.collectNestedProcSigs(proc.Body)
   156→		}
   157→		if fn, ok := stmt.(*ast.FuncDecl); ok {
   158→			g.procSigs[fn.Name] = fn.Params
   159→		}
   160→		if proto, ok := stmt.(*ast.ProtocolDecl); ok {
   161→			g.protocolDefs[proto.Name] = proto
   162→		}
   163→		if rec, ok := stmt.(*ast.RecordDecl); ok {
   164→			g.recordDefs[rec.Name] = rec
   165→		}
   166→		g.collectChanProtocols(stmt)
   167→		g.collectRecordVars(stmt)
   168→	}
   169→
   170→	// Separate protocol, record, procedure declarations from other statements
   171→	var typeDecls []ast.Statement
   172→	var procDecls []ast.Statement
   173→	var mainStatements []ast.Statement
   174→
   175→	// First pass: check if there are any proc/func declarations
   176→	hasProcDecls := false
   177→	for _, stmt := range program.Statements {
   178→		if _, ok := stmt.(*ast.ProcDecl); ok {
   179→			hasProcDecls = true
   180→			break
   181→		}
   182→		if _, ok := stmt.(*ast.FuncDecl); ok {
   183→			hasProcDecls = true
   184→			break
   185→		}
   186→	}
   187→
   188→	var abbrDecls []ast.Statement
   189→	for _, stmt := range program.Statements {
   190→		switch s := stmt.(type) {
   191→		case *ast.ProtocolDecl, *ast.RecordDecl:
   192→			typeDecls = append(typeDecls, stmt)
   193→		case *ast.ProcDecl, *ast.FuncDecl:
   194→			procDecls = append(procDecls, stmt)
   195→		case *ast.Abbreviation:
   196→			if hasProcDecls {
   197→				// Top-level abbreviations need to be at package level
   198→				// so PROCs can reference them
   199→				abbrDecls = append(abbrDecls, stmt)
   200→			} else {
   201→				mainStatements = append(mainStatements, stmt)
   202→			}
   203→		case *ast.RetypesDecl:
   204→			_ = s
   205→			// RETYPES declarations are local to functions, not package-level
   206→			mainStatements = append(mainStatements, stmt)
   207→		default:
   208→			mainStatements = append(mainStatements, stmt)
   209→		}
   210→	}
   211→
   212→	// Detect entry point PROC so we can set import flags before writing imports
   213→	var entryProc *ast.ProcDecl
   214→	if len(mainStatements) == 0 {
   215→		entryProc = g.findEntryProc(procDecls)
   216→		if entryProc != nil {
   217→			g.needOs = true
   218→			g.needSync = true
   219→			g.needBufio = true
   220→		}
   221→	}
   222→
   223→	// Write package declaration
   224→	g.writeLine("package main")
   225→	g.writeLine("")
   226→
   227→	// Write imports
   228→	if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits || g.needBufio || g.needReflect {
   229→		g.writeLine("import (")
   230→		g.indent++
   231→		if g.needBufio {
   232→			g.writeLine(`"bufio"`)
   233→		}
   234→		if g.needFmt {
   235→			g.writeLine(`"fmt"`)
   236→		}
   237→		if g.needMath {
   238→			g.writeLine(`"math"`)
   239→		}
   240→		if g.needMathBits {
   241→			g.writeLine(`"math/bits"`)
   242→		}
   243→		if g.needOs {
   244→			g.writeLine(`"os"`)
   245→		}
   246→		if g.needReflect {
   247→			g.writeLine(`"reflect"`)
   248→		}
   249→		if g.needSync {
   250→			g.writeLine(`"sync"`)
   251→		}
   252→		if g.needTime {
   253→			g.writeLine(`"time"`)
   254→		}
   255→		g.indent--
   256→		g.writeLine(")")
   257→		g.writeLine("")
   258→	}
   259→
   260→	// Emit transputer intrinsic helper functions
   261→	if g.needMathBits {
   262→		g.emitIntrinsicHelpers()
   263→	}
   264→
   265→	// Emit _boolToInt helper function
   266→	if g.needBoolHelper {
   267→		g.emitBoolHelper()
   268→	}
   269→
   270→	// Generate type definitions first (at package level)
   271→	for _, stmt := range typeDecls {
   272→		g.generateStatement(stmt)
   273→	}
   274→
   275→	// Generate package-level abbreviations (constants)
   276→	for _, stmt := range abbrDecls {
   277→		abbr := stmt.(*ast.Abbreviation)
   278→		if abbr.Type == "" {
   279→			// Untyped VAL: let Go infer the type
   280→			g.builder.WriteString("var ")
   281→			g.write(fmt.Sprintf("%s = ", goIdent(abbr.Name)))
   282→			g.generateExpression(abbr.Value)
   283→			g.write("\n")
   284→		} else {
   285→			goType := g.occamTypeToGo(abbr.Type)
   286→			if abbr.IsOpenArray || abbr.IsFixedArray {
   287→				goType = "[]" + goType
   288→			}
   289→			g.builder.WriteString("var ")
   290→			g.write(fmt.Sprintf("%s %s = ", goIdent(abbr.Name), goType))
   291→			// Wrap string literals with []byte() when assigned to []byte variables
   292→			if _, isStr := abbr.Value.(*ast.StringLiteral); isStr && abbr.IsOpenArray && abbr.Type == "BYTE" {
   293→				g.write("[]byte(")
   294→				g.generateExpression(abbr.Value)
   295→				g.write(")")
   296→			} else {
   297→				g.generateExpression(abbr.Value)
   298→			}
   299→			g.write("\n")
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go"
}
     1→package codegen
     2→
     3→import (
     4→	"os"
     5→	"os/exec"
     6→	"path/filepath"
     7→	"testing"
     8→
     9→	"github.com/codeassociates/occam2go/lexer"
    10→	"github.com/codeassociates/occam2go/parser"
    11→	"github.com/codeassociates/occam2go/preproc"
    12→)
    13→
    14→// transpileCompileRun takes Occam source, transpiles to Go, compiles, runs,
    15→// and returns the stdout output
    16→func transpileCompileRun(t *testing.T, occamSource string) string {
    17→	t.Helper()
    18→
    19→	// Transpile
    20→	l := lexer.New(occamSource)
    21→	p := parser.New(l)
    22→	program := p.ParseProgram()
    23→
    24→	if len(p.Errors()) > 0 {
    25→		for _, err := range p.Errors() {
    26→			t.Errorf("parser error: %s", err)
    27→		}
    28→		t.FailNow()
    29→	}
    30→
    31→	gen := New()
    32→	goCode := gen.Generate(program)
    33→
    34→	// Create temp directory for this test
    35→	tmpDir, err := os.MkdirTemp("", "occam2go-test-*")
    36→	if err != nil {
    37→		t.Fatalf("failed to create temp dir: %v", err)
    38→	}
    39→	defer os.RemoveAll(tmpDir)
    40→
    41→	// Write Go source
    42→	goFile := filepath.Join(tmpDir, "main.go")
    43→	if err := os.WriteFile(goFile, []byte(goCode), 0644); err != nil {
    44→		t.Fatalf("failed to write Go file: %v", err)
    45→	}
    46→
    47→	// Compile
    48→	binFile := filepath.Join(tmpDir, "main")
    49→	compileCmd := exec.Command("go", "build", "-o", binFile, goFile)
    50→	compileOutput, err := compileCmd.CombinedOutput()
    51→	if err != nil {
    52→		t.Fatalf("compilation failed: %v\nOutput: %s\nGo code:\n%s", err, compileOutput, goCode)
    53→	}
    54→
    55→	// Run
    56→	runCmd := exec.Command(binFile)
    57→	output, err := runCmd.CombinedOutput()
    58→	if err != nil {
    59→		t.Fatalf("execution failed: %v\nOutput: %s", err, output)
    60→	}
    61→
    62→	return string(output)
    63→}
    64→
    65→// transpileCompileRunFromFile takes an occam file path, preprocesses it,
    66→// then transpiles, compiles, and runs.
    67→func transpileCompileRunFromFile(t *testing.T, mainFile string, includePaths []string) string {
    68→	t.Helper()
    69→
    70→	pp := preproc.New(preproc.WithIncludePaths(includePaths))
    71→	expanded, err := pp.ProcessFile(mainFile)
    72→	if err != nil {
    73→		t.Fatalf("preprocessor error: %v", err)
    74→	}
    75→	if len(pp.Errors()) > 0 {
    76→		for _, e := range pp.Errors() {
    77→			t.Errorf("preprocessor warning: %s", e)
    78→		}
    79→	}
    80→
    81→	return transpileCompileRun(t, expanded)
    82→}
    83→

Now let me read the entry harness generation code and the rest of the import/flag sections.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 395,
  "limit": 150
}
   395→// findEntryProc looks for the last top-level PROC with the standard occam
   396→// entry point signature: exactly 3 CHAN OF BYTE params (keyboard?, screen!, error!).
   397→func (g *Generator) findEntryProc(procDecls []ast.Statement) *ast.ProcDecl {
   398→	var entry *ast.ProcDecl
   399→	for _, stmt := range procDecls {
   400→		proc, ok := stmt.(*ast.ProcDecl)
   401→		if !ok {
   402→			continue
   403→		}
   404→		if len(proc.Params) != 3 {
   405→			continue
   406→		}
   407→		p0, p1, p2 := proc.Params[0], proc.Params[1], proc.Params[2]
   408→		if p0.IsChan && p0.ChanElemType == "BYTE" && p0.ChanDir == "?" &&
   409→			p1.IsChan && p1.ChanElemType == "BYTE" && p1.ChanDir == "!" &&
   410→			p2.IsChan && p2.ChanElemType == "BYTE" && p2.ChanDir == "!" {
   411→			entry = proc
   412→		}
   413→	}
   414→	return entry
   415→}
   416→
   417→// generateEntryHarness emits a func main() that wires stdin/stdout/stderr
   418→// to channels and calls the entry PROC.
   419→func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
   420→	name := goIdent(proc.Name)
   421→	g.writeLine("func main() {")
   422→	g.indent++
   423→
   424→	// Create channels
   425→	g.writeLine("keyboard := make(chan byte, 256)")
   426→	g.writeLine("screen := make(chan byte, 256)")
   427→	g.writeLine("_error := make(chan byte, 256)")
   428→	g.writeLine("")
   429→
   430→	// WaitGroup for writer goroutines to finish draining
   431→	g.writeLine("var wg sync.WaitGroup")
   432→	g.writeLine("wg.Add(2)")
   433→	g.writeLine("")
   434→
   435→	// Screen writer goroutine
   436→	g.writeLine("go func() {")
   437→	g.indent++
   438→	g.writeLine("defer wg.Done()")
   439→	g.writeLine("w := bufio.NewWriter(os.Stdout)")
   440→	g.writeLine("for b := range screen {")
   441→	g.indent++
   442→	g.writeLine("if b == 255 {")
   443→	g.indent++
   444→	g.writeLine("w.Flush()")
   445→	g.indent--
   446→	g.writeLine("} else {")
   447→	g.indent++
   448→	g.writeLine("w.WriteByte(b)")
   449→	g.indent--
   450→	g.writeLine("}")
   451→	g.indent--
   452→	g.writeLine("}")
   453→	g.writeLine("w.Flush()")
   454→	g.indent--
   455→	g.writeLine("}()")
   456→	g.writeLine("")
   457→
   458→	// Error writer goroutine
   459→	g.writeLine("go func() {")
   460→	g.indent++
   461→	g.writeLine("defer wg.Done()")
   462→	g.writeLine("w := bufio.NewWriter(os.Stderr)")
   463→	g.writeLine("for b := range _error {")
   464→	g.indent++
   465→	g.writeLine("if b == 255 {")
   466→	g.indent++
   467→	g.writeLine("w.Flush()")
   468→	g.indent--
   469→	g.writeLine("} else {")
   470→	g.indent++
   471→	g.writeLine("w.WriteByte(b)")
   472→	g.indent--
   473→	g.writeLine("}")
   474→	g.indent--
   475→	g.writeLine("}")
   476→	g.writeLine("w.Flush()")
   477→	g.indent--
   478→	g.writeLine("}()")
   479→	g.writeLine("")
   480→
   481→	// Keyboard reader goroutine
   482→	g.writeLine("go func() {")
   483→	g.indent++
   484→	g.writeLine("r := bufio.NewReader(os.Stdin)")
   485→	g.writeLine("for {")
   486→	g.indent++
   487→	g.writeLine("b, err := r.ReadByte()")
   488→	g.writeLine("if err != nil {")
   489→	g.indent++
   490→	g.writeLine("close(keyboard)")
   491→	g.writeLine("return")
   492→	g.indent--
   493→	g.writeLine("}")
   494→	g.writeLine("keyboard <- b")
   495→	g.indent--
   496→	g.writeLine("}")
   497→	g.indent--
   498→	g.writeLine("}()")
   499→	g.writeLine("")
   500→
   501→	// Call the entry proc
   502→	g.writeLine(fmt.Sprintf("%s(keyboard, screen, _error)", name))
   503→	g.writeLine("")
   504→
   505→	// Close output channels and wait for writers to drain
   506→	g.writeLine("close(screen)")
   507→	g.writeLine("close(_error)")
   508→	g.writeLine("wg.Wait()")
   509→
   510→	g.indent--
   511→	g.writeLine("}")
   512→}
   513→
   514→func (g *Generator) containsPar(stmt ast.Statement) bool {
   515→	switch s := stmt.(type) {
   516→	case *ast.ParBlock:
   517→		return true
   518→	case *ast.SeqBlock:
   519→		for _, inner := range s.Statements {
   520→			if g.containsPar(inner) {
   521→				return true
   522→			}
   523→		}
   524→	case *ast.AltBlock:
   525→		for _, c := range s.Cases {
   526→			for _, inner := range c.Body {
   527→				if g.containsPar(inner) {
   528→					return true
   529→				}
   530→			}
   531→		}
   532→	case *ast.ProcDecl:
   533→		for _, inner := range s.Body {
   534→			if g.containsPar(inner) {
   535→				return true
   536→			}
   537→		}
   538→	case *ast.FuncDecl:
   539→		for _, inner := range s.Body {
   540→			if g.containsPar(inner) {
   541→				return true
   542→			}
   543→		}
   544→	case *ast.WhileLoop:
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen_test.go",
  "limit": 50
}
     1→package codegen
     2→
     3→import (
     4→	"strings"
     5→	"testing"
     6→
     7→	"github.com/codeassociates/occam2go/lexer"
     8→	"github.com/codeassociates/occam2go/parser"
     9→)
    10→
    11→func TestSimpleVarDecl(t *testing.T) {
    12→	input := `INT x:
    13→`
    14→	output := transpile(t, input)
    15→
    16→	if !strings.Contains(output, "var x int") {
    17→		t.Errorf("expected 'var x int' in output, got:\n%s", output)
    18→	}
    19→}
    20→
    21→func TestMultipleVarDecl(t *testing.T) {
    22→	input := `INT x, y, z:
    23→`
    24→	output := transpile(t, input)
    25→
    26→	if !strings.Contains(output, "var x, y, z int") {
    27→		t.Errorf("expected 'var x, y, z int' in output, got:\n%s", output)
    28→	}
    29→}
    30→
    31→func TestReal32VarDecl(t *testing.T) {
    32→	input := `REAL32 x:
    33→`
    34→	output := transpile(t, input)
    35→
    36→	if !strings.Contains(output, "var x float32") {
    37→		t.Errorf("expected 'var x float32' in output, got:\n%s", output)
    38→	}
    39→}
    40→
    41→func TestReal64VarDecl(t *testing.T) {
    42→	input := `REAL64 x:
    43→`
    44→	output := transpile(t, input)
    45→
    46→	if !strings.Contains(output, "var x float64") {
    47→		t.Errorf("expected 'var x float64' in output, got:\n%s", output)
    48→	}
    49→}
    50→

Now let me create the task list and start implementing.

TaskCreate
Run go get golang.org/x/term to add the dependency to go.mod/go.sum
{
  "subject": "Add golang.org/x/term dependency",
  "activeForm": "Adding golang.org/x/term dependency"
}
Task #1 created successfully: Add golang.org/x/term dependency
TaskCreate
Add needTerm bool flag to Generator struct, initialize it, set it when entry harness detected, add imports for os/signal, syscall, golang.org/x/term, and rewrite generateEntryHarness() for raw terminal mode
{
  "subject": "Update codegen.go: needTerm flag, imports, entry harness",
  "activeForm": "Updating codegen.go for raw terminal mode"
}
Task #2 created successfully: Update codegen.go: needTerm flag, imports, entry harness
TaskCreate
Add codegen unit test verifying entry-harness-style program generates code with term.MakeRaw, term.IsTerminal, term.Restore
{
  "subject": "Add unit test for entry harness term.MakeRaw",
  "activeForm": "Adding unit test for raw terminal mode"
}
Task #3 created successfully: Add unit test for entry harness term.MakeRaw
TaskCreate
Add transpileCompileRunWithInput helper and e2e test that pipes input to an echoing-style program, verifying output matches
{
  "subject": "Add e2e test with piped stdin",
  "activeForm": "Adding e2e test with piped stdin"
}
Task #4 created successfully: Add e2e test with piped stdin
TaskCreate
Run go test ./... and ensure all tests pass
{
  "subject": "Run tests and verify",
  "activeForm": "Running tests"
}
Task #5 created successfully: Run tests and verify

Now let's start. First, add the dependency.

TaskUpdate
{
  "taskId": "1",
  "status": "in_progress"
}
Updated task #1 status
$ Bash
Add golang.org/x/term dependency
go get golang.org/x/term
go: downloading golang.org/x/term v0.40.0
go: downloading golang.org/x/sys v0.41.0
go: added golang.org/x/sys v0.41.0
go: added golang.org/x/term v0.40.0
TaskUpdate
{
  "taskId": "1",
  "status": "completed"
}
Updated task #1 status

Now implement the codegen changes.

TaskUpdate
{
  "taskId": "2",
  "status": "in_progress"
}
Updated task #2 status

Step 1: Add needTerm flag to Generator struct

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	needReflect    bool // track if we need reflect package import
	needBoolHelper bool // track if we need _boolToInt helper
+
	needReflect    bool // track if we need reflect package import
	needBoolHelper bool // track if we need _boolToInt helper
	needTerm       bool // track if we need golang.org/x/term package import
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Step 2: Initialize the flag

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	g.needReflect = false
	g.needBoolHelper = false
+
	g.needReflect = false
	g.needBoolHelper = false
	g.needTerm = false
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Step 3: Set needTerm when entry harness is detected

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
		if entryProc != nil {
			g.needOs = true
			g.needSync = true
			g.needBufio = true
		}
+
		if entryProc != nil {
			g.needOs = true
			g.needSync = true
			g.needBufio = true
			g.needTerm = true
		}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Step 4: Update import generation

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits || g.needBufio || g.needReflect {
		g.writeLine("import (")
		g.indent++
		if g.needBufio {
			g.writeLine(`"bufio"`)
		}
		if g.needFmt {
			g.writeLine(`"fmt"`)
		}
		if g.needMath {
			g.writeLine(`"math"`)
		}
		if g.needMathBits {
			g.writeLine(`"math/bits"`)
		}
		if g.needOs {
			g.writeLine(`"os"`)
		}
		if g.needReflect {
			g.writeLine(`"reflect"`)
		}
		if g.needSync {
			g.writeLine(`"sync"`)
		}
		if g.needTime {
			g.writeLine(`"time"`)
		}
		g.indent--
		g.writeLine(")")
		g.writeLine("")
	}
+
	if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits || g.needBufio || g.needReflect || g.needTerm {
		g.writeLine("import (")
		g.indent++
		if g.needBufio {
			g.writeLine(`"bufio"`)
		}
		if g.needFmt {
			g.writeLine(`"fmt"`)
		}
		if g.needMath {
			g.writeLine(`"math"`)
		}
		if g.needMathBits {
			g.writeLine(`"math/bits"`)
		}
		if g.needOs {
			g.writeLine(`"os"`)
		}
		if g.needTerm {
			g.writeLine(`"os/signal"`)
		}
		if g.needReflect {
			g.writeLine(`"reflect"`)
		}
		if g.needSync {
			g.writeLine(`"sync"`)
		}
		if g.needTerm {
			g.writeLine(`"syscall"`)
		}
		if g.needTime {
			g.writeLine(`"time"`)
		}
		if g.needTerm {
			g.writeLine("")
			g.writeLine(`"golang.org/x/term"`)
		}
		g.indent--
		g.writeLine(")")
		g.writeLine("")
	}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Step 5: Rewrite generateEntryHarness()

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
// generateEntryHarness emits a func main() that wires stdin/stdout/stderr
// to channels and calls the entry PROC.
func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
	name := goIdent(proc.Name)
	g.writeLine("func main() {")
	g.indent++

	// Create channels
	g.writeLine("keyboard := make(chan byte, 256)")
	g.writeLine("screen := make(chan byte, 256)")
	g.writeLine("_error := make(chan byte, 256)")
	g.writeLine("")

	// WaitGroup for writer goroutines to finish draining
	g.writeLine("var wg sync.WaitGroup")
	g.writeLine("wg.Add(2)")
	g.writeLine("")

	// Screen writer goroutine
	g.writeLine("go func() {")
	g.indent++
	g.writeLine("defer wg.Done()")
	g.writeLine("w := bufio.NewWriter(os.Stdout)")
	g.writeLine("for b := range screen {")
	g.indent++
	g.writeLine("if b == 255 {")
	g.indent++
	g.writeLine("w.Flush()")
	g.indent--
	g.writeLine("} else {")
	g.indent++
	g.writeLine("w.WriteByte(b)")
	g.indent--
	g.writeLine("}")
	g.indent--
	g.writeLine("}")
	g.writeLine("w.Flush()")
	g.indent--
	g.writeLine("}()")
	g.writeLine("")

	// Error writer goroutine
	g.writeLine("go func() {")
	g.indent++
	g.writeLine("defer wg.Done()")
	g.writeLine("w := bufio.NewWriter(os.Stderr)")
	g.writeLine("for b := range _error {")
	g.indent++
	g.writeLine("if b == 255 {")
	g.indent++
	g.writeLine("w.Flush()")
	g.indent--
	g.writeLine("} else {")
	g.indent++
	g.writeLine("w.WriteByte(b)")
	g.indent--
	g.writeLine("}")
	g.indent--
	g.writeLine("}")
	g.writeLine("w.Flush()")
	g.indent--
	g.writeLine("}()")
	g.writeLine("")

	// Keyboard reader goroutine
	g.writeLine("go func() {")
	g.indent++
	g.writeLine("r := bufio.NewReader(os.Stdin)")
	g.writeLine("for {")
	g.indent++
	g.writeLine("b, err := r.ReadByte()")
	g.writeLine("if err != nil {")
	g.indent++
	g.writeLine("close(keyboard)")
	g.writeLine("return")
	g.indent--
	g.writeLine("}")
	g.writeLine("keyboard <- b")
	g.indent--
	g.writeLine("}")
	g.indent--
	g.writeLine("}()")
	g.writeLine("")

	// Call the entry proc
	g.writeLine(fmt.Sprintf("%s(keyboard, screen, _error)", name))
	g.writeLine("")

	// Close output channels and wait for writers to drain
	g.writeLine("close(screen)")
	g.writeLine("close(_error)")
	g.writeLine("wg.Wait()")

	g.indent--
	g.writeLine("}")
}
+
// generateEntryHarness emits a func main() that wires stdin/stdout/stderr
// to channels and calls the entry PROC.  When stdin is a terminal, the
// harness switches to raw mode (via golang.org/x/term) so that keyboard
// input is available character-by-character without waiting for Enter.
func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
	name := goIdent(proc.Name)
	g.writeLine("func main() {")
	g.indent++

	// Create channels
	g.writeLine("keyboard := make(chan byte, 256)")
	g.writeLine("screen := make(chan byte, 256)")
	g.writeLine("_error := make(chan byte, 256)")
	g.writeLine("")

	// Raw terminal mode setup
	g.writeLine("// Raw terminal mode — gives character-at-a-time keyboard input")
	g.writeLine("var rawMode bool")
	g.writeLine("var oldState *term.State")
	g.writeLine("fd := int(os.Stdin.Fd())")
	g.writeLine("if term.IsTerminal(fd) {")
	g.indent++
	g.writeLine("var err error")
	g.writeLine("oldState, err = term.MakeRaw(fd)")
	g.writeLine("if err == nil {")
	g.indent++
	g.writeLine("rawMode = true")
	g.writeLine("defer term.Restore(fd, oldState)")
	g.writeLine("// Restore terminal on external signals")
	g.writeLine("sigCh := make(chan os.Signal, 1)")
	g.writeLine("signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)")
	g.writeLine("go func() {")
	g.indent++
	g.writeLine("<-sigCh")
	g.writeLine("term.Restore(fd, oldState)")
	g.writeLine("os.Exit(1)")
	g.indent--
	g.writeLine("}()")
	g.indent--
	g.writeLine("}")
	g.indent--
	g.writeLine("}")
	g.writeLine("")

	// WaitGroup for writer goroutines to finish draining
	g.writeLine("var wg sync.WaitGroup")
	g.writeLine("wg.Add(2)")
	g.writeLine("")

	// Screen writer goroutine — in raw mode, insert CR before LF
	g.writeLine("go func() {")
	g.indent++
	g.writeLine("defer wg.Done()")
	g.writeLine("w := bufio.NewWriter(os.Stdout)")
	g.writeLine("for b := range screen {")
	g.indent++
	g.writeLine("if b == 255 {")
	g.indent++
	g.writeLine("w.Flush()")
	g.indent--
	g.writeLine("} else {")
	g.indent++
	g.writeLine(`if rawMode && b == '\n' {`)
	g.writeLine(`w.WriteByte('\r')`)
	g.writeLine("}")
	g.writeLine("w.WriteByte(b)")
	g.indent--
	g.writeLine("}")
	g.indent--
	g.writeLine("}")
	g.writeLine("w.Flush()")
	g.indent--
	g.writeLine("}()")
	g.writeLine("")

	// Error writer goroutine — same CR/LF handling
	g.writeLine("go func() {")
	g.indent++
	g.writeLine("defer wg.Done()")
	g.writeLine("w := bufio.NewWriter(os.Stderr)")
	g.writeLine("for b := range _error {")
	g.indent++
	g.writeLine("if b == 255 {")
	g.indent++
	g.writeLine("w.Flush()")
	g.indent--
	g.writeLine("} else {")
	g.indent++
	g.writeLine(`if rawMode && b == '\n' {`)
	g.writeLine(`w.WriteByte('\r')`)
	g.writeLine("}")
	g.writeLine("w.WriteByte(b)")
	g.indent--
	g.writeLine("}")
	g.indent--
	g.writeLine("}")
	g.writeLine("w.Flush()")
	g.indent--
	g.writeLine("}()")
	g.writeLine("")

	// Keyboard reader goroutine
	g.writeLine("go func() {")
	g.indent++
	g.writeLine("if rawMode {")
	g.indent++
	g.writeLine("buf := make([]byte, 1)")
	g.writeLine("for {")
	g.indent++
	g.writeLine("n, err := os.Stdin.Read(buf)")
	g.writeLine("if err != nil || n == 0 {")
	g.indent++
	g.writeLine("close(keyboard)")
	g.writeLine("return")
	g.indent--
	g.writeLine("}")
	g.writeLine("if buf[0] == 3 { // Ctrl+C")
	g.indent++
	g.writeLine("term.Restore(fd, oldState)")
	g.writeLine("os.Exit(1)")
	g.indent--
	g.writeLine("}")
	g.writeLine("keyboard <- buf[0]")
	g.indent--
	g.writeLine("}")
	g.indent--
	g.writeLine("} else {")
	g.indent++
	g.writeLine("r := bufio.NewReader(os.Stdin)")
	g.writeLine("for {")
	g.indent++
	g.writeLine("b, err := r.ReadByte()")
	g.writeLine("if err != nil {")
	g.indent++
	g.writeLine("close(keyboard)")
	g.writeLine("return")
	g.indent--
	g.writeLine("}")
	g.writeLine("keyboard <- b")
	g.indent--
	g.writeLine("}")
	g.indent--
	g.writeLine("}")
	g.indent--
	g.writeLine("}()")
	g.writeLine("")

	// Call the entry proc
	g.writeLine(fmt.Sprintf("%s(keyboard, screen, _error)", name))
	g.writeLine("")

	// Close output channels and wait for writers to drain
	g.writeLine("close(screen)")
	g.writeLine("close(_error)")
	g.writeLine("wg.Wait()")

	g.indent--
	g.writeLine("}")
}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
TaskUpdate
{
  "taskId": "2",
  "status": "completed"
}
Updated task #2 status

Now add the unit test. Let me find the transpile helper first and the end of the test file.

Grep
{
  "pattern": "^func transpile\\(",
  "path": "/home/david/projects/code-associates/occam2go/codegen",
  "output_mode": "content"
}
codegen/codegen_test.go:204:func transpile(t *testing.T, input string) string {
$ Bash
Get line count of codegen_test.go
wc -l /home/david/projects/code-associates/occam2go/codegen/codegen_test.go
854 /home/david/projects/code-associates/occam2go/codegen/codegen_test.go
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen_test.go",
  "offset": 840
}
   840→	output := transpile(t, input)
   841→	if !strings.Contains(output, "grid[i][j] = 42") {
   842→		t.Errorf("expected 'grid[i][j] = 42' in output, got:\n%s", output)
   843→	}
   844→}
   845→
   846→func TestMultiDimProcParamCodegen(t *testing.T) {
   847→	input := `PROC fill([][]CHAN OF INT grid)
   848→  SKIP
   849→`
   850→	output := transpile(t, input)
   851→	if !strings.Contains(output, "func fill(grid [][]chan int)") {
   852→		t.Errorf("expected 'func fill(grid [][]chan int)' in output, got:\n%s", output)
   853→	}
   854→}
   855→
TaskUpdate
{
  "taskId": "3",
  "status": "in_progress"
}
Updated task #3 status
✏️ Edit codegen_test.go
/home/david/projects/code-associates/occam2go/codegen/codegen_test.go
func TestMultiDimProcParamCodegen(t *testing.T) {
	input := `PROC fill([][]CHAN OF INT grid)
  SKIP
`
	output := transpile(t, input)
	if !strings.Contains(output, "func fill(grid [][]chan int)") {
		t.Errorf("expected 'func fill(grid [][]chan int)' in output, got:\n%s", output)
	}
}
+
func TestMultiDimProcParamCodegen(t *testing.T) {
	input := `PROC fill([][]CHAN OF INT grid)
  SKIP
`
	output := transpile(t, input)
	if !strings.Contains(output, "func fill(grid [][]chan int)") {
		t.Errorf("expected 'func fill(grid [][]chan int)' in output, got:\n%s", output)
	}
}

func TestEntryHarnessRawTerminal(t *testing.T) {
	input := `PROC echo(CHAN OF BYTE keyboard?, screen!, error!)
  BYTE ch:
  SEQ
    keyboard ? ch
    screen ! ch
:
`
	output := transpile(t, input)

	// Should contain raw terminal mode setup
	for _, want := range []string{
		"term.IsTerminal",
		"term.MakeRaw",
		"term.Restore",
		`"golang.org/x/term"`,
		`"os/signal"`,
		`"syscall"`,
		"rawMode",
	} {
		if !strings.Contains(output, want) {
			t.Errorf("expected %q in entry harness output, got:\n%s", want, output)
		}
	}
}
The file /home/david/projects/code-associates/occam2go/codegen/codegen_test.go has been updated successfully.
TaskUpdate
{
  "taskId": "3",
  "status": "completed"
}
Updated task #3 status

Now add the e2e test with piped stdin.

TaskUpdate
{
  "taskId": "4",
  "status": "in_progress"
}
Updated task #4 status

Let me check the e2e_test.go for existing patterns.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_test.go",
  "limit": 60
}
File does not exist. Note: your current working directory is /home/david/projects/code-associates/occam2go.
Glob
{
  "pattern": "codegen/*e2e*",
  "path": "/home/david/projects/code-associates/occam2go"
}
/home/david/projects/code-associates/occam2go/codegen/e2e_preproc_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_record_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_proc_func_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_replicator_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_basic_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_phase2_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_course_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_intrinsics_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_strings_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_params_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_retypes_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_types_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_array_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_misc_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go

Let me look at an existing entry harness e2e test to understand the pattern.

Grep
{
  "pattern": "entry|harness|Entry|Harness|CHAN OF BYTE keyboard\"",
  "path": "/home/david/projects/code-associates/occam2go/codegen",
  "glob": "*e2e*test.go",
  "output_mode": "content"
}
No matches found
Grep
{
  "pattern": "CHAN OF BYTE\"",
  "path": "/home/david/projects/code-associates/occam2go/codegen",
  "glob": "*e2e*test.go",
  "output_mode": "content"
}
No matches found
Grep
{
  "pattern": "CHAN OF BYTE",
  "path": "/home/david/projects/code-associates/occam2go/codegen",
  "glob": "*e2e*test.go",
  "output_mode": "content"
}
codegen/e2e_concurrency_test.go:336:  [2]CHAN OF BYTE cs:

No existing entry harness e2e tests yet. The entry harness program needs go mod init + go mod tidy since it imports golang.org/x/term. Let me add the helper and test.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go"
}
     1→package codegen
     2→
     3→import (
     4→	"os"
     5→	"os/exec"
     6→	"path/filepath"
     7→	"testing"
     8→
     9→	"github.com/codeassociates/occam2go/lexer"
    10→	"github.com/codeassociates/occam2go/parser"
    11→	"github.com/codeassociates/occam2go/preproc"
    12→)
    13→
    14→// transpileCompileRun takes Occam source, transpiles to Go, compiles, runs,
    15→// and returns the stdout output
    16→func transpileCompileRun(t *testing.T, occamSource string) string {
    17→	t.Helper()
    18→
    19→	// Transpile
    20→	l := lexer.New(occamSource)
    21→	p := parser.New(l)
    22→	program := p.ParseProgram()
    23→
    24→	if len(p.Errors()) > 0 {
    25→		for _, err := range p.Errors() {
    26→			t.Errorf("parser error: %s", err)
    27→		}
    28→		t.FailNow()
    29→	}
    30→
    31→	gen := New()
    32→	goCode := gen.Generate(program)
    33→
    34→	// Create temp directory for this test
    35→	tmpDir, err := os.MkdirTemp("", "occam2go-test-*")
    36→	if err != nil {
    37→		t.Fatalf("failed to create temp dir: %v", err)
    38→	}
    39→	defer os.RemoveAll(tmpDir)
    40→
    41→	// Write Go source
    42→	goFile := filepath.Join(tmpDir, "main.go")
    43→	if err := os.WriteFile(goFile, []byte(goCode), 0644); err != nil {
    44→		t.Fatalf("failed to write Go file: %v", err)
    45→	}
    46→
    47→	// Compile
    48→	binFile := filepath.Join(tmpDir, "main")
    49→	compileCmd := exec.Command("go", "build", "-o", binFile, goFile)
    50→	compileOutput, err := compileCmd.CombinedOutput()
    51→	if err != nil {
    52→		t.Fatalf("compilation failed: %v\nOutput: %s\nGo code:\n%s", err, compileOutput, goCode)
    53→	}
    54→
    55→	// Run
    56→	runCmd := exec.Command(binFile)
    57→	output, err := runCmd.CombinedOutput()
    58→	if err != nil {
    59→		t.Fatalf("execution failed: %v\nOutput: %s", err, output)
    60→	}
    61→
    62→	return string(output)
    63→}
    64→
    65→// transpileCompileRunFromFile takes an occam file path, preprocesses it,
    66→// then transpiles, compiles, and runs.
    67→func transpileCompileRunFromFile(t *testing.T, mainFile string, includePaths []string) string {
    68→	t.Helper()
    69→
    70→	pp := preproc.New(preproc.WithIncludePaths(includePaths))
    71→	expanded, err := pp.ProcessFile(mainFile)
    72→	if err != nil {
    73→		t.Fatalf("preprocessor error: %v", err)
    74→	}
    75→	if len(pp.Errors()) > 0 {
    76→		for _, e := range pp.Errors() {
    77→			t.Errorf("preprocessor warning: %s", e)
    78→		}
    79→	}
    80→
    81→	return transpileCompileRun(t, expanded)
    82→}
    83→
✏️ Edit e2e_helpers_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go
import (
	"os"
	"os/exec"
	"path/filepath"
	"testing"

	"github.com/codeassociates/occam2go/lexer"
	"github.com/codeassociates/occam2go/parser"
	"github.com/codeassociates/occam2go/preproc"
)
+
import (
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"github.com/codeassociates/occam2go/lexer"
	"github.com/codeassociates/occam2go/parser"
	"github.com/codeassociates/occam2go/preproc"
)
The file /home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go has been updated successfully.

Now add the transpileCompileRunWithInput helper at the end of the file.

✏️ Edit e2e_helpers_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go
	return transpileCompileRun(t, expanded)
}
+
	return transpileCompileRun(t, expanded)
}

// transpileCompileRunWithInput takes Occam source that uses the entry-point
// PROC pattern (CHAN OF BYTE keyboard?, screen!, error!), transpiles to Go,
// initialises a Go module (needed for golang.org/x/term), compiles, pipes
// the given input to stdin, and returns the stdout output.
func transpileCompileRunWithInput(t *testing.T, occamSource, stdin string) string {
	t.Helper()

	// Transpile
	l := lexer.New(occamSource)
	p := parser.New(l)
	program := p.ParseProgram()

	if len(p.Errors()) > 0 {
		for _, err := range p.Errors() {
			t.Errorf("parser error: %s", err)
		}
		t.FailNow()
	}

	gen := New()
	goCode := gen.Generate(program)

	// Create temp directory
	tmpDir, err := os.MkdirTemp("", "occam2go-test-*")
	if err != nil {
		t.Fatalf("failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tmpDir)

	// Write Go source
	goFile := filepath.Join(tmpDir, "main.go")
	if err := os.WriteFile(goFile, []byte(goCode), 0644); err != nil {
		t.Fatalf("failed to write Go file: %v", err)
	}

	// Initialise Go module (needed for golang.org/x/term dependency)
	modInit := exec.Command("go", "mod", "init", "test")
	modInit.Dir = tmpDir
	if out, err := modInit.CombinedOutput(); err != nil {
		t.Fatalf("go mod init failed: %v\n%s", err, out)
	}
	modTidy := exec.Command("go", "mod", "tidy")
	modTidy.Dir = tmpDir
	if out, err := modTidy.CombinedOutput(); err != nil {
		t.Fatalf("go mod tidy failed: %v\n%s\nGo code:\n%s", err, out, goCode)
	}

	// Compile
	binFile := filepath.Join(tmpDir, "main")
	compileCmd := exec.Command("go", "build", "-o", binFile, ".")
	compileCmd.Dir = tmpDir
	if out, err := compileCmd.CombinedOutput(); err != nil {
		t.Fatalf("compilation failed: %v\nOutput: %s\nGo code:\n%s", err, out, goCode)
	}

	// Run with piped stdin
	runCmd := exec.Command(binFile)
	runCmd.Stdin = strings.NewReader(stdin)
	output, err := runCmd.CombinedOutput()
	if err != nil {
		t.Fatalf("execution failed: %v\nOutput: %s", err, output)
	}

	return string(output)
}
The file /home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go has been updated successfully.

Now add the e2e test. Let me put it in a new file to keep things organized.

📝 Write e2e_harness_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_harness_test.go
package codegen

import "testing"

func TestE2EEntryHarnessEcho(t *testing.T) {
	// An echoing program that reads characters until 'Z' and echoes each one.
	// Uses the standard occam entry-point PROC signature.
	input := `PROC echo(CHAN OF BYTE keyboard?, screen!, error!)
  BYTE ch:
  SEQ
    keyboard ? ch
    WHILE ch <> 'Z'
      SEQ
        screen ! ch
        keyboard ? ch
:
`
	// Pipe "hello Z" — the program should echo "hello " (everything before Z)
	output := transpileCompileRunWithInput(t, input, "hello Z")

	expected := "hello "
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}
File created successfully at: /home/david/projects/code-associates/occam2go/codegen/e2e_harness_test.go
TaskUpdate
{
  "taskId": "4",
  "status": "completed"
}
Updated task #4 status

Now let's run the tests.

TaskUpdate
{
  "taskId": "5",
  "status": "in_progress"
}
Updated task #5 status
$ Bash
Run the unit test for raw terminal harness
go test ./codegen -run TestEntryHarnessRawTerminal -v 2>&1 | head -40
=== RUN   TestEntryHarnessRawTerminal
--- PASS: TestEntryHarnessRawTerminal (0.00s)
PASS
ok  	github.com/codeassociates/occam2go/codegen	0.001s

Unit test passes. Now the e2e test (this will be slower due to go mod tidy downloading deps).

$ Bash
Run the e2e entry harness test
go test ./codegen -run TestE2EEntryHarnessEcho -v 2>&1 | tail -30
=== RUN   TestE2EEntryHarnessEcho
--- PASS: TestE2EEntryHarnessEcho (0.59s)
PASS
ok  	github.com/codeassociates/occam2go/codegen	0.594s

Both new tests pass. Now let's run the full test suite.

$ Bash
Run all tests
go test ./... 2>&1 | tail -30
# github.com/codeassociates/occam2go
./test.go:2689:6: main redeclared in this block
	./main.go:29:6: other declaration of main
FAIL	github.com/codeassociates/occam2go [build failed]
?   	github.com/codeassociates/occam2go/ast	[no test files]
ok  	github.com/codeassociates/occam2go/codegen	19.107s
ok  	github.com/codeassociates/occam2go/lexer	(cached)
ok  	github.com/codeassociates/occam2go/modgen	(cached)
ok  	github.com/codeassociates/occam2go/parser	(cached)
ok  	github.com/codeassociates/occam2go/preproc	(cached)
FAIL

The only failure is the pre-existing test.go file in the repo root (noted in git status as untracked) which conflicts with main.go. That's unrelated to our changes. All the codegen, lexer, parser, modgen, and preproc tests pass.

Let me verify the course module still works too.

$ Bash
Verify course module transpiles and passes go vet
go build -o /tmp/occam2go . && /tmp/occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/course_out.go kroc/modules/course/libsrc/course.module && go vet /tmp/course_out.go 2>&1
Exit code 1
# github.com/codeassociates/occam2go
./test.go:2689:6: main redeclared in this block
	./main.go:29:6: other declaration of main

# github.com/codeassociates/occam2go
./test.go:2689:6: main redeclared in this block
	./main.go:29:6: other declaration of main

The untracked test.go in root is blocking go build. Let me use a different approach.

$ Bash
Build from main.go only, then verify course module
go build -o /tmp/occam2go ./main.go && /tmp/occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/course_out.go kroc/modules/course/libsrc/course.module && go vet /tmp/course_out.go 2>&1

Course module still transpiles and passes go vet.

TaskUpdate
{
  "taskId": "5",
  "status": "completed"
}
Updated task #5 status

All done. Here's a summary of the changes:

Changes made

  1. go.mod / go.sum — Added golang.org/x/term (and transitive golang.org/x/sys) dependency

  2. codegen/codegen.go — Three areas changed:

  3. Added needTerm bool field to Generator struct (line ~22) and initialized it in Generate() (line ~108)
  4. Set g.needTerm = true when entry harness is detected (line ~221)
  5. Updated import block to emit "os/signal", "syscall", and "golang.org/x/term" when needTerm is set
  6. Rewrote generateEntryHarness() to:

    • Put terminal in raw mode via term.MakeRaw() when stdin is a terminal
    • Fall back to bufio.NewReader when stdin is piped (non-terminal)
    • Insert \r before \n in screen/error writers when in raw mode (raw mode disables OPOST)
    • Intercept Ctrl+C (byte 3) in raw mode keyboard reader to restore terminal and exit
    • Handle SIGINT/SIGTERM to restore terminal state
  7. codegen/codegen_test.go — Added TestEntryHarnessRawTerminal unit test verifying generated code contains term.MakeRaw, term.IsTerminal, term.Restore, and the new imports

  8. codegen/e2e_helpers_test.go — Added transpileCompileRunWithInput() helper that initializes a Go module (for x/term dep), compiles, and pipes stdin to the binary

  9. codegen/e2e_harness_test.go — New e2e test TestE2EEntryHarnessEcho that transpiles an echoing program, pipes "hello Z", and verifies output is "hello "

Test results

  • All codegen, lexer, parser, modgen, and preproc tests pass
  • Course module still transpiles and passes go vet
  • The root-level build failure is from a pre-existing untracked test.go file, unrelated to these changes

That's great. I did a simple manual test and it looks to be working. Please create a PR for this new feature.

Let me check the current state of the repo first.

$ Bash
Show working tree status
git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   codegen/codegen.go
	modified:   codegen/codegen_test.go
	modified:   codegen/e2e_helpers_test.go
	modified:   go.mod

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	codegen/e2e_harness_test.go
	go.sum
	od
	test.go

no changes added to commit (use "git add" and/or "git commit -a")
$ Bash
Show unstaged changes
git diff
diff --git a/codegen/codegen.go b/codegen/codegen.go
index 4211f74..e91bc12 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -20,6 +20,7 @@ type Generator struct {
 	needBufio    bool // track if we need bufio package import
 	needReflect    bool // track if we need reflect package import
 	needBoolHelper bool // track if we need _boolToInt helper
+	needTerm       bool // track if we need golang.org/x/term package import
 
 	// Track procedure signatures for proper pointer handling
 	procSigs map[string][]ast.ProcParam
@@ -105,6 +106,7 @@ func (g *Generator) Generate(program *ast.Program) string {
 	g.needBufio = false
 	g.needReflect = false
 	g.needBoolHelper = false
+	g.needTerm = false
 	g.procSigs = make(map[string][]ast.ProcParam)
 	g.refParams = make(map[string]bool)
 	g.protocolDefs = make(map[string]*ast.ProtocolDecl)
@@ -217,6 +219,7 @@ func (g *Generator) Generate(program *ast.Program) string {
 			g.needOs = true
 			g.needSync = true
 			g.needBufio = true
+			g.needTerm = true
 		}
 	}
 
@@ -225,7 +228,7 @@ func (g *Generator) Generate(program *ast.Program) string {
 	g.writeLine("")
 
 	// Write imports
-	if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits || g.needBufio || g.needReflect {
+	if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits || g.needBufio || g.needReflect || g.needTerm {
 		g.writeLine("import (")
 		g.indent++
 		if g.needBufio {
@@ -243,15 +246,25 @@ func (g *Generator) Generate(program *ast.Program) string {
 		if g.needOs {
 			g.writeLine(`"os"`)
 		}
+		if g.needTerm {
+			g.writeLine(`"os/signal"`)
+		}
 		if g.needReflect {
 			g.writeLine(`"reflect"`)
 		}
 		if g.needSync {
 			g.writeLine(`"sync"`)
 		}
+		if g.needTerm {
+			g.writeLine(`"syscall"`)
+		}
 		if g.needTime {
 			g.writeLine(`"time"`)
 		}
+		if g.needTerm {
+			g.writeLine("")
+			g.writeLine(`"golang.org/x/term"`)
+		}
 		g.indent--
 		g.writeLine(")")
 		g.writeLine("")
@@ -415,7 +428,9 @@ func (g *Generator) findEntryProc(procDecls []ast.Statement) *ast.ProcDecl {
 }
 
 // generateEntryHarness emits a func main() that wires stdin/stdout/stderr
-// to channels and calls the entry PROC.
+// to channels and calls the entry PROC.  When stdin is a terminal, the
+// harness switches to raw mode (via golang.org/x/term) so that keyboard
+// input is available character-by-character without waiting for Enter.
 func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
 	name := goIdent(proc.Name)
 	g.writeLine("func main() {")
@@ -427,12 +442,41 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
 	g.writeLine("_error := make(chan byte, 256)")
 	g.writeLine("")
 
+	// Raw terminal mode setup
+	g.writeLine("// Raw terminal mode — gives character-at-a-time keyboard input")
+	g.writeLine("var rawMode bool")
+	g.writeLine("var oldState *term.State")
+	g.writeLine("fd := int(os.Stdin.Fd())")
+	g.writeLine("if term.IsTerminal(fd) {")
+	g.indent++
+	g.writeLine("var err error")
+	g.writeLine("oldState, err = term.MakeRaw(fd)")
+	g.writeLine("if err == nil {")
+	g.indent++
+	g.writeLine("rawMode = true")
+	g.writeLine("defer term.Restore(fd, oldState)")
+	g.writeLine("// Restore terminal on external signals")
+	g.writeLine("sigCh := make(chan os.Signal, 1)")
+	g.writeLine("signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)")
+	g.writeLine("go func() {")
+	g.indent++
+	g.writeLine("<-sigCh")
+	g.writeLine("term.Restore(fd, oldState)")
+	g.writeLine("os.Exit(1)")
+	g.indent--
+	g.writeLine("}()")
+	g.indent--
+	g.writeLine("}")
+	g.indent--
+	g.writeLine("}")
+	g.writeLine("")
+
 	// WaitGroup for writer goroutines to finish draining
 	g.writeLine("var wg sync.WaitGroup")
 	g.writeLine("wg.Add(2)")
 	g.writeLine("")
 
-	// Screen writer goroutine
+	// Screen writer goroutine — in raw mode, insert CR before LF
 	g.writeLine("go func() {")
 	g.indent++
 	g.writeLine("defer wg.Done()")
@@ -445,6 +489,9 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
 	g.indent--
 	g.writeLine("} else {")
 	g.indent++
+	g.writeLine(`if rawMode && b == '\n' {`)
+	g.writeLine(`w.WriteByte('\r')`)
+	g.writeLine("}")
 	g.writeLine("w.WriteByte(b)")
 	g.indent--
 	g.writeLine("}")
@@ -455,7 +502,7 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
 	g.writeLine("}()")
 	g.writeLine("")
 
-	// Error writer goroutine
+	// Error writer goroutine — same CR/LF handling
 	g.writeLine("go func() {")
 	g.indent++
 	g.writeLine("defer wg.Done()")
@@ -468,6 +515,9 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
 	g.indent--
 	g.writeLine("} else {")
 	g.indent++
+	g.writeLine(`if rawMode && b == '\n' {`)
+	g.writeLine(`w.WriteByte('\r')`)
+	g.writeLine("}")
 	g.writeLine("w.WriteByte(b)")
 	g.indent--
 	g.writeLine("}")
@@ -481,6 +531,30 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
 	// Keyboard reader goroutine
 	g.writeLine("go func() {")
 	g.indent++
+	g.writeLine("if rawMode {")
+	g.indent++
+	g.writeLine("buf := make([]byte, 1)")
+	g.writeLine("for {")
+	g.indent++
+	g.writeLine("n, err := os.Stdin.Read(buf)")
+	g.writeLine("if err != nil || n == 0 {")
+	g.indent++
+	g.writeLine("close(keyboard)")
+	g.writeLine("return")
+	g.indent--
+	g.writeLine("}")
+	g.writeLine("if buf[0] == 3 { // Ctrl+C")
+	g.indent++
+	g.writeLine("term.Restore(fd, oldState)")
+	g.writeLine("os.Exit(1)")
+	g.indent--
+	g.writeLine("}")
+	g.writeLine("keyboard <- buf[0]")
+	g.indent--
+	g.writeLine("}")
+	g.indent--
+	g.writeLine("} else {")
+	g.indent++
 	g.writeLine("r := bufio.NewReader(os.Stdin)")
 	g.writeLine("for {")
 	g.indent++
@@ -495,6 +569,8 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
 	g.indent--
 	g.writeLine("}")
 	g.indent--
+	g.writeLine("}")
+	g.indent--
 	g.writeLine("}()")
 	g.writeLine("")
 
diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go
index 4882a43..486e9a5 100644
--- a/codegen/codegen_test.go
+++ b/codegen/codegen_test.go
@@ -852,3 +852,29 @@ func TestMultiDimProcParamCodegen(t *testing.T) {
 		t.Errorf("expected 'func fill(grid [][]chan int)' in output, got:\n%s", output)
 	}
 }
+
+func TestEntryHarnessRawTerminal(t *testing.T) {
+	input := `PROC echo(CHAN OF BYTE keyboard?, screen!, error!)
+  BYTE ch:
+  SEQ
+    keyboard ? ch
+    screen ! ch
+:
+`
+	output := transpile(t, input)
+
+	// Should contain raw terminal mode setup
+	for _, want := range []string{
+		"term.IsTerminal",
+		"term.MakeRaw",
+		"term.Restore",
+		`"golang.org/x/term"`,
+		`"os/signal"`,
+		`"syscall"`,
+		"rawMode",
+	} {
+		if !strings.Contains(output, want) {
+			t.Errorf("expected %q in entry harness output, got:\n%s", want, output)
+		}
+	}
+}
diff --git a/codegen/e2e_helpers_test.go b/codegen/e2e_helpers_test.go
index 47f0f55..dae94ca 100644
--- a/codegen/e2e_helpers_test.go
+++ b/codegen/e2e_helpers_test.go
@@ -4,6 +4,7 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"strings"
 	"testing"
 
 	"github.com/codeassociates/occam2go/lexer"
@@ -80,3 +81,69 @@ func transpileCompileRunFromFile(t *testing.T, mainFile string, includePaths []s
 
 	return transpileCompileRun(t, expanded)
 }
+
+// transpileCompileRunWithInput takes Occam source that uses the entry-point
+// PROC pattern (CHAN OF BYTE keyboard?, screen!, error!), transpiles to Go,
+// initialises a Go module (needed for golang.org/x/term), compiles, pipes
+// the given input to stdin, and returns the stdout output.
+func transpileCompileRunWithInput(t *testing.T, occamSource, stdin string) string {
+	t.Helper()
+
+	// Transpile
+	l := lexer.New(occamSource)
+	p := parser.New(l)
+	program := p.ParseProgram()
+
+	if len(p.Errors()) > 0 {
+		for _, err := range p.Errors() {
+			t.Errorf("parser error: %s", err)
+		}
+		t.FailNow()
+	}
+
+	gen := New()
+	goCode := gen.Generate(program)
+
+	// Create temp directory
+	tmpDir, err := os.MkdirTemp("", "occam2go-test-*")
+	if err != nil {
+		t.Fatalf("failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	// Write Go source
+	goFile := filepath.Join(tmpDir, "main.go")
+	if err := os.WriteFile(goFile, []byte(goCode), 0644); err != nil {
+		t.Fatalf("failed to write Go file: %v", err)
+	}
+
+	// Initialise Go module (needed for golang.org/x/term dependency)
+	modInit := exec.Command("go", "mod", "init", "test")
+	modInit.Dir = tmpDir
+	if out, err := modInit.CombinedOutput(); err != nil {
+		t.Fatalf("go mod init failed: %v\n%s", err, out)
+	}
+	modTidy := exec.Command("go", "mod", "tidy")
+	modTidy.Dir = tmpDir
+	if out, err := modTidy.CombinedOutput(); err != nil {
+		t.Fatalf("go mod tidy failed: %v\n%s\nGo code:\n%s", err, out, goCode)
+	}
+
+	// Compile
+	binFile := filepath.Join(tmpDir, "main")
+	compileCmd := exec.Command("go", "build", "-o", binFile, ".")
+	compileCmd.Dir = tmpDir
+	if out, err := compileCmd.CombinedOutput(); err != nil {
+		t.Fatalf("compilation failed: %v\nOutput: %s\nGo code:\n%s", err, out, goCode)
+	}
+
+	// Run with piped stdin
+	runCmd := exec.Command(binFile)
+	runCmd.Stdin = strings.NewReader(stdin)
+	output, err := runCmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("execution failed: %v\nOutput: %s", err, output)
+	}
+
+	return string(output)
+}
diff --git a/go.mod b/go.mod
index 70c29f1..38cf26c 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,8 @@
 module github.com/codeassociates/occam2go
 
 go 1.25.6
+
+require (
+	golang.org/x/sys v0.41.0 // indirect
+	golang.org/x/term v0.40.0 // indirect
+)
$ Bash
Show recent commit messages
git log --oneline -5
814db69 Merge pull request #89 from codeassociates/fix/life-occ-compilation-72
5ab69cb Use entry point PROC pattern for life.occ terminal I/O
126b182 Fix Game of Life example compilation (#72)
3540129 Merge pull request #88 from codeassociates/fix/receive-indexed-target-85
58a1542 Support receive into indexed target variables (#85)

Good. Now let me create the branch, commit, and PR.

$ Bash
Create feature branch
git checkout -b feat/raw-terminal-keyboard-90
Switched to a new branch 'feat/raw-terminal-keyboard-90'
$ Bash
Stage changed and new files
git add codegen/codegen.go codegen/codegen_test.go codegen/e2e_helpers_test.go codegen/e2e_harness_test.go go.mod go.sum
$ Bash
Commit the raw terminal mode changes
git commit -m "$(cat <<'EOF'
Raw terminal mode for keyboard channel (#90)

Use golang.org/x/term to switch stdin to raw mode in the generated entry
harness so characters are available immediately as typed, without waiting
for Enter. Falls back to buffered bufio.NewReader when stdin is not a
terminal (piped input). Handles Ctrl+C in raw mode, LF→CRLF in output
writers, and restores terminal state on signals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
6 files changed, 207 insertions(+), 4 deletions(-)
 create mode 100644 codegen/e2e_harness_test.go
 create mode 100644 go.sum
$ Bash
Push feature branch to remote
git push -u origin feat/raw-terminal-keyboard-90
remote: 
remote: Create a pull request for 'feat/raw-terminal-keyboard-90' on GitHub by visiting:        
remote:      https://github.com/codeassociates/occam2go/pull/new/feat/raw-terminal-keyboard-90        
remote: 
To associate-1.github.com:codeassociates/occam2go.git
 * [new branch]      feat/raw-terminal-keyboard-90 -> feat/raw-terminal-keyboard-90
branch 'feat/raw-terminal-keyboard-90' set up to track 'origin/feat/raw-terminal-keyboard-90'.
$ Bash
Create pull request
gh pr create --title "Raw terminal mode for keyboard channel" --body "$(cat <<'EOF'
## Summary
- Use `golang.org/x/term` to switch stdin to raw mode in the generated entry harness, so keyboard input is available character-by-character without waiting for Enter (#90)
- Fall back to buffered `bufio.NewReader` when stdin is piped (non-terminal)
- Handle Ctrl+C (byte 3) in raw mode to restore terminal and exit cleanly
- Insert CR before LF in screen/error output writers when in raw mode (raw mode disables OPOST)
- Restore terminal state on SIGINT/SIGTERM via signal handler

## Test plan
- [x] Unit test verifies generated code contains `term.MakeRaw`, `term.IsTerminal`, `term.Restore`, and new imports
- [x] E2E test transpiles an echoing program, pipes `"hello Z"` to stdin, verifies output matches `"hello "`
- [x] All existing tests pass (`go test ./codegen ./lexer ./parser ./preproc ./modgen`)
- [x] Course module still transpiles and passes `go vet`
- [x] Manual test with `echoing.occ` — characters echo immediately on keypress

Closes #90

🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
Warning: 2 uncommitted changes
https://github.com/codeassociates/occam2go/pull/91

PR created: https://github.com/codeassociates/occam2go/pull/91

Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.

/exit exit

Bye!