coach

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 14, 2026 License: BSD-2-Clause

README

coach

coach is an experimental AI coach designed to help software engineers and autonomous agents build better software. It parses source code syntactically and flags code smells, design issues, and structural metrics.

Packages

  • pkg/semantics — Deterministic structural analysis of Go, TypeScript, and TSX source bytes (validates syntax, extracts imports, computes branching metrics, and detects constructor-like patterns).
  • pkg/githubingest — Optional GitHub App-authenticated single-file reader via the GitHub Contents API.

Installation

Go Packages

To install the Go packages:

go get github.com/lousy-agents/coach/pkg/semantics

# Optional: for GitHub App content ingestion
go get github.com/lousy-agents/coach/pkg/githubingest
JavaScript / TypeScript Bindings (@lousy-agents/coach-semantics)

The JS/TS bindings are currently packaged for Node.js (ESM-only).

[!NOTE] Because coach is in an active experimental phase, the npm package is not yet published to the public npm registry. To consume it, you must clone the repository and build the library locally:

[!IMPORTANT] Build Prerequisites: Because the package compiles its underlying parser engine locally during installation, you must have:

  • Node.js (>= 20)
  • Go (>= 1.25.0)
  • C Toolchain (e.g., GCC or Clang): The Go parser engine compiles with CGO enabled by default. If a C toolchain is not available on your system, you can disable CGO by setting CGO_ENABLED=0 (e.g., CGO_ENABLED=0 npm install) to build using the pure-Go engine fallback automatically.
  1. Clone and Build:

    git clone https://github.com/lousy-agents/coach.git
    cd coach/js/semantics
    npm install   # Compiles the underlying parser engine and packages TS code
    
  2. Link or Reference the Package: In your client application, add the local path as a dependency:

    cd ~/your-app
    npm install /path/to/coach/js/semantics
    

coach codesignal CLI Preview

cmd/coach provides a codesignal subcommand: a local, deterministic preview of pkg/codesignal you can run directly against a Git checkout, without any GitHub App, worker, or model/LLM configuration.

Download the latest coach binary for macOS from the GitHub Releases page. Each tagged release publishes separate archives for Apple silicon (darwin_arm64) and x86-64 Intel Macs (darwin_x86_64), a checksums.txt file, and cosign signature/certificate files.

ARCH=darwin_arm64  # or darwin_x86_64

curl -LO https://github.com/lousy-agents/coach/releases/latest/download/coach_${ARCH}.tar.gz
curl -LO https://github.com/lousy-agents/coach/releases/latest/download/checksums.txt
curl -LO https://github.com/lousy-agents/coach/releases/latest/download/checksums.txt.sig
curl -LO https://github.com/lousy-agents/coach/releases/latest/download/checksums.txt.pem

# Verify the checksums file was signed by this repository's release workflow
cosign verify-blob \
  --signature checksums.txt.sig \
  --certificate checksums.txt.pem \
  --certificate-identity-regexp '^https://github.com/lousy-agents/coach/.github/workflows/release.yml@refs/tags/v.*$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt

# Verify the archive against the signed checksums
shasum -a 256 -c --ignore-missing checksums.txt

# Extract the binary
tar -xzf coach_${ARCH}.tar.gz

Move the extracted coach binary somewhere on your PATH.

[!NOTE] From a local clone, you can still build/install it with:

go install ./cmd/coach

Prerequisites: a local Git checkout and the git executable in PATH. Building from source also requires Go 1.25+ (matching go.mod).

Usage

Run it from inside a Git worktree, pointing --base at the revision you want to diff against:

coach codesignal --base <ref>
  • --base is required; it can be any ref Git can resolve to a commit (a branch, tag, or SHA).
  • --format defaults to text; pass --format=json for machine-readable output.

Text example (coach codesignal --base <ref>), after a commit adds a function that mutates a caller-owned pointer:

files analyzed: 1, active signals: 1, diagnostics: 0
path: config.go
line: 12
lifecycle: introduced
changed: true
evidence: cfg.Timeout
why it matters: Mutating a caller-owned input can create behavior that is not visible from the function signature, make outcomes dependent on call ordering, introduce temporal coupling, make tests and local reasoning more difficult, and surprise callers that expect an input to remain unchanged.
recommendation: Return a copy instead of mutating the caller's value, or document/rename this function to make the in-place mutation explicit.

JSON example (coach codesignal --base <ref> --format=json), for the same change:

{
  "schema_version": "1",
  "scope": { "revision": "3474e2c...", "base": "ece3690..." },
  "summary": {
    "files_analyzed": 1,
    "files_with_diagnostics": 0,
    "active_signals": 1,
    "introduced_signals": 1,
    "existing_signals": 0,
    "resolved_signals": 0
  },
  "signals": [
    {
      "id": "sig_88ec28c6...",
      "fingerprint": "fp_dcf2afc2...",
      "rule_id": "state.hidden_input_mutation",
      "rule_version": "1",
      "kind": "hidden_input_mutation",
      "category": "state_management",
      "severity": "medium",
      "confidence": "medium",
      "lifecycle": "introduced",
      "changed": true,
      "path": "config.go",
      "subject": "ApplyDefaults:cfg",
      "location": { "start_byte": 137, "end_byte": 148, "start_row": 11, "start_col": 1, "end_row": 11, "end_col": 12 },
      "evidence": "cfg.Timeout",
      "why_it_matters": "Mutating a caller-owned input can create behavior that is not visible from the function signature, make outcomes dependent on call ordering, introduce temporal coupling, make tests and local reasoning more difficult, and surprise callers that expect an input to remain unchanged.",
      "recommendation": "Return a copy instead of mutating the caller's value, or document/rename this function to make the in-place mutation explicit.",
      "suggested_skill": "refactor-hidden-mutation",
      "provenance": { "producer": "semantics", "finding_kind": "mutates_input" }
    }
  ]
}
Exit status
  • 0 — the CLI completed its analysis, regardless of whether any signals or diagnostics were reported. A quiet report with only diagnostics and zero signals is still a normal, exit-0 outcome.
  • 1 — an operational failure: the working directory is not a Git worktree, --base cannot be resolved, git is not found in PATH, or an internal analysis step fails. One actionable message goes to stderr; nothing is written to stdout.
  • 2 — a usage error: --base is missing, or --format is not text or json. Usage guidance goes to stderr; nothing is written to stdout.
Scope and limitations
  • Advisory only. It surfaces deterministic structural signals; it does not judge correctness or block anything on its own.
  • Go, TypeScript, and TSX only. Changed files in other languages are skipped (with an unsupported_language diagnostic).
  • Does not execute code. All analysis is static, over source bytes read via git show.
  • No runtime proof, no cross-file analysis. It cannot prove a defect exists or trace causality across files; each file is analyzed independently.
  • Local-only, zero external configuration. It never contacts GitHub, a model/LLM API, or any other network service — see internal/codesignalcli/dependencies_test.go's TestNoExternalDependencies for the enforced boundary (no net/http, no GitHub client, anywhere in its dependency graph).
  • Renames and copies are not analyzed for lifecycle continuity. A renamed or copied file produces an unsupported_change_type diagnostic instead of being diffed against its old path.
Signal lifecycle and changed

Every signal carries a lifecycle, computed by comparing HEAD against the resolved merge-base:

  • introduced — present at HEAD, not present at the merge-base.
  • existing — present at both HEAD and the merge-base.
  • resolved — present at the merge-base, not present at HEAD.
  • unknown — the merge-base side of the file could not be analyzed (e.g. it had a syntax error), so lifecycle can't be determined.

changed is a separate boolean: it's true when the signal's HEAD location overlaps a line the diff marks as changed, independent of lifecycle — an untouched, pre-existing signal in a file that had other lines changed is changed: false.

Locations: 0-based JSON vs 1-based text

JSON output reports Location fields (start_row, start_col, etc., see pkg/semantics/result.go) as 0-based, matching Tree-sitter's own convention. Text-mode output's line: field adds 1 to start_row for a human-friendly 1-based display line.


pkg/semantics Quickstart

pkg/semantics operates purely on raw bytes, meaning you don't need a file system to analyze code.

package main

import (
	"context"
	"fmt"

	"github.com/lousy-agents/coach/pkg/semantics"
)

func main() {
	// Initialize the analyzer
	analyzer, err := semantics.NewAnalyzer(semantics.AnalyzerOptions{})
	if err != nil {
		panic(err)
	}

	sourceBytes := []byte(`
		package main
		func Hello() string {
			return "world"
		}
	`)

	result, err := analyzer.AnalyzeBytes(context.Background(), semantics.FileInput{
		Path:     "hello.go",
		Language: semantics.LanguageGo,
		Content:  sourceBytes,
	})
	if err != nil {
		panic(err)
	}

	fmt.Println("Status:", result.ParseStatus) // "ok"
	for _, f := range result.Findings {
		fmt.Printf("Finding: %s (Location: %v)\n", f.Kind, f.Location)
	}
}
Error Handling

If a file has syntax errors, AnalyzeBytes returns a partial *Result along with an error wrapped as ErrSyntax. You can extract detailed syntax issues using errors.As:

import "errors"

// ...

result, err := analyzer.AnalyzeBytes(ctx, input)
if errors.Is(err, semantics.ErrSyntax) {
	var syntaxErr *semantics.SyntaxError
	if errors.As(err, &syntaxErr) {
		for _, issue := range syntaxErr.Issues {
			fmt.Printf("Syntax issue: %s at %v\n", issue.Kind, issue.Location)
		}
	}
}

Other sentinel errors:

  • ErrEmptyContent — The provided input content is empty.
  • ErrUnsupportedLanguage — The file extension or language is not supported.
  • ErrFileTooLarge — The source file exceeds limits.
  • ErrBinaryContent — The file appears to be binary.
  • ErrParseFailure — General Tree-sitter parsing failure.

Coaching Findings

Beyond parsing metrics, coach analyzes code constructs to flag specific design issues. The most common is mutates_input.

mutates_input Finding

mutates_input flags functions or methods that mutate a parameter in place. This can lead to hard-to-debug "spooky action at a distance" bugs.

For example, this Go function mutates its input in place:

func ApplyDefaults(cfg *Config) {
    cfg.Timeout = 30 * time.Second // Mutates the caller's Config
}

This generates a finding detailing the mutation:

{
  "kind": "mutates_input",
  "name": "ApplyDefaults:cfg",
  "location": { "start_byte": 26, "end_byte": 36, "start_row": 1, "start_col": 4, "end_row": 1, "end_col": 14 },
  "confidence": "medium",
  "evidence": "cfg.Timeout",
  "recommendation": "Return a copy instead of mutating the caller's value, or document/rename this function to make the in-place mutation explicit.",
  "suggested_skill": "refactor-hidden-mutation"
}

A safer alternative returns the updated value:

func WithDefaults(cfg Config) Config {
    cfg.Timeout = 30 * time.Second
    return cfg
}

[!TIP] Syntactic Constraint: The mutates_input checker is purely syntactic and conservative. It does not perform interprocedural analysis or follow reference aliases across scopes.

Other Supported Findings
  • constructor_func (Go) — Flags factory or constructor functions.
  • pointer_return (Go) — Flags functions returning a pointer to a struct.
  • tight_coupling (TS/TSX) — Flags class/object patterns with high coupling.

JavaScript / TypeScript Quickstart

Here is how you can perform syntactic analysis in a Node.js project:

import { readFile } from "node:fs/promises";
import { createAnalyzer, SemanticsSyntaxError } from "@lousy-agents/coach-semantics";

// Initialize the analyzer child process
const analyzer = await createAnalyzer();

try {
  const content = await readFile("widget.ts");
  const result = await analyzer.analyzeBytes({
    path: "widget.ts",
    language: "typescript", // "go", "typescript", or "tsx"
    content: content,
  });

  console.log("Status:", result.parse_status);
  console.log("Metrics:", result.metrics);
  console.log("Findings:", result.findings);
} catch (err) {
  if (err instanceof SemanticsSyntaxError) {
    // Access the partial Result containing syntax errors
    console.log("Syntax Errors:", err.partialResult.syntax_errors);
  } else {
    console.error("Analysis failed:", err);
    throw err;
  }
} finally {
  // Always clean up the analyzer process when finished
  analyzer.dispose();
}

Thrown errors inherit from SemanticsError and carry a kind string:

  • "syntax", "empty_content", "unsupported_language", "file_too_large", "binary_content", "parse_failure", "invalid_options", "canceled", "internal", "backend_unavailable".

Stability Guarantees

  • JSON Stability: The output structure and its snake_case JSON field names are completely frozen.
  • API Stability: Because coach is currently pre-1.0, the core JSON structure and sentinel errors are stable, but other parts of the API surface may evolve.

Directories

Path Synopsis
cmd
coach command
Command coach is the composition-root CLI for the coach project.
Command coach is the composition-root CLI for the coach project.
codesignal-report command
Command codesignal-report reads a batch of NDJSON lines from stdin, accumulates a single codesignal.Input, and writes exactly one JSON codesignal.Report to stdout.
Command codesignal-report reads a batch of NDJSON lines from stdin, accumulates a single codesignal.Input, and writes exactly one JSON codesignal.Report to stdout.
semantics-json command
Command semantics-json exposes internal/jsbridge as a long-running newline-delimited-JSON server on stdin/stdout, so non-Go hosts (the js/semantics package) can call pkg/semantics without linking CGO themselves.
Command semantics-json exposes internal/jsbridge as a long-running newline-delimited-JSON server on stdin/stdout, so non-Go hosts (the js/semantics package) can call pkg/semantics without linking CGO themselves.
semantics-wasm-smoke command
Stub entry point so `go build ./...` / `go vet ./...` on native platforms don't fail with "build constraints exclude all Go files".
Stub entry point so `go build ./...` / `go vet ./...` on native platforms don't fail with "build constraints exclude all Go files".
internal
codesignalcli
Package codesignalcli implements the Git-facing plumbing behind the `coach codesignal` subcommand: resolving revisions and selecting the files a CodeSignal report should analyze.
Package codesignalcli implements the Git-facing plumbing behind the `coach codesignal` subcommand: resolving revisions and selecting the files a CodeSignal report should analyze.
jsbridge
Package jsbridge defines the JSON protocol that exposes pkg/semantics to JavaScript/TypeScript consumers, plus the single handler both transport backends (WASM and stdio CLI) call.
Package jsbridge defines the JSON protocol that exposes pkg/semantics to JavaScript/TypeScript consumers, plus the single handler both transport backends (WASM and stdio CLI) call.
pkg
codesignal
Package codesignal turns pkg/semantics analysis results into prioritized, change-aware coaching signals.
Package codesignal turns pkg/semantics analysis results into prioritized, change-aware coaching signals.
githubingest
Package githubingest reads individual file contents from a GitHub repository via the GitHub Contents API, authenticated as a GitHub App installation.
Package githubingest reads individual file contents from a GitHub repository via the GitHub Contents API, authenticated as a GitHub App installation.
semantics
Package semantics extracts deterministic structural facts from raw source bytes (syntax validity, imports, branching metrics, constructor-like patterns, mutates_input) using Tree-sitter grammars.
Package semantics extracts deterministic structural facts from raw source bytes (syntax validity, imports, branching metrics, constructor-like patterns, mutates_input) using Tree-sitter grammars.
semantics/internal/engine
The CGO backend: adapts github.com/tree-sitter/go-tree-sitter (which binds Tree-sitter's C runtime) to the engine interfaces.
The CGO backend: adapts github.com/tree-sitter/go-tree-sitter (which binds Tree-sitter's C runtime) to the engine interfaces.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL