From: Anderson A. Giacomolli Date: Fri, 6 Nov 2026 12:50:06 +0000 (-0300) Subject: Add Nix-built Go program and agent rules X-Git-Url: http://git.giacomolli.uk/onechat/commitdiff_plain/80fa33c0fd65e4110f88f0cab9b19ca26c27d1f6 Add Nix-built Go program and agent rules OneChat is a Go program that prints its name. A Nix flake pins the Go toolchain and every tool, builds the program with nix build, and runs the binary in an install check. The development shell supplies the formatters, the linters, and a pinned Vim, and it turns on the commit message hook. AGENTS.md holds the rules that apply to every task, such as the Nix environment, the current-state-only rule, and the rule against revealing infrastructure. The skills in .agents/skills hold the procedures for Go code, documentation and comments, checks, and commits, so an agent loads each one only for the task that needs it. bin/format and bin/check cover every Go, Nix, RST, and Markdown file that Git knows about. bin/reflow wraps comment paragraphs the same way on every machine because it does not use a personal Vim configuration. CLAUDE.md and .claude/skills are links to the same files, so Claude Code reads one copy of the rules and skills. --- 80fa33c0fd65e4110f88f0cab9b19ca26c27d1f6 diff --git a/.agents/skills/check/SKILL.md b/.agents/skills/check/SKILL.md new file mode 100644 index 0000000..1526edb --- /dev/null +++ b/.agents/skills/check/SKILL.md @@ -0,0 +1,23 @@ +--- +name: check +description: Format, lint, test, and build. Use before every commit. +--- + +# Check + +Run every command in the Nix development shell. Use +`nix develop -c COMMAND` when direnv does not load the shell. + +## Procedure + +1. Format all files with `bin/format`. +2. Run the checks with `bin/check`. +3. Run `git add` on new files, then run `nix build` and + `nix flake check`. +4. Fix every failure and run the steps again. Do not disable a linter or + add a `nolint` directive to get a pass, unless the user agrees. +5. Read the final diff and compare it with `AGENTS.md` and the skills of + the task. Passing tools do not replace this review. + +Tell the user about each check that cannot run and each failure that +stays. diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md new file mode 100644 index 0000000..6676a85 --- /dev/null +++ b/.agents/skills/commit/SKILL.md @@ -0,0 +1,61 @@ +--- +name: commit +description: Commit procedure and message rules. Use for every commit. +--- + +# Commit + +## Procedure + +1. Do the `check` skill. All checks must pass before you commit. +2. Stage only the files that belong to this change. Keep unrelated + changes in separate commits. +3. Write the message to a file or a here-document and commit with + `git commit -F`. Never put `\n` in `git commit -m`. +4. When `.githooks/commit-msg` rejects the message, fix the message and + commit again. Do not skip the hook with `--no-verify`. + +## Size + +- Commit often. Split the work into small, coherent, working steps, and + do not wait for a full feature. +- Before a push, rebase on the latest `master` and fix conflicts during + the rebase. Never make a merge commit. + +## Subject + +- Use 50 characters or fewer. +- Write a short summary in the present-tense imperative mood. +- Capitalize the first word. Do not end with a period. +- Do not use a subsystem or prefix. + +## Body + +- Separate the subject from the body with exactly one blank line. +- Always write a body. Wrap each line at 72 characters. Fill each line + of a paragraph before you wrap it. +- Write in the present-tense declarative mood. +- Start with a direct summary of the complete change. Do not only repeat + the subject. +- Explain what the change does and why it is necessary. Describe the + resulting behavior and design. +- Do not describe the development process, list the changed files, or + give obvious implementation steps. + +## Trailers + +- When the change belongs to an issue, end the message with one `Link:` + trailer to that issue. Add it with `git commit --trailer`. Never use a + closing keyword such as "Fixes" or "Closes". +- Link the bug report when a change fixes a reported bug. +- Do not add an AI tool as co-author. Do not add attribution trailers. + +## Example + +``` +Print the program name + +OneChat prints its name and exits. The program gives the Nix package a +binary that the install check can run, so the build pipeline is tested +before the first feature. +``` diff --git a/.agents/skills/docs/SKILL.md b/.agents/skills/docs/SKILL.md new file mode 100644 index 0000000..b802f77 --- /dev/null +++ b/.agents/skills/docs/SKILL.md @@ -0,0 +1,41 @@ +--- +name: docs +description: Docs and comment rules. Use before you write any prose. +--- + +# Documentation and comments + +## Documentation + +- `README.rst` is the entry point. It says what OneChat is, its status, + and how to build, run, and check it. Keep it short. +- Do not add other documentation files or a `docs/` folder. Document + commands and flags in the command help, and code contracts and design + reasons in Go doc comments. +- Use Markdown only for files that a tool requires by name, such as + `AGENTS.md` and `SKILL.md`. + +## Lists + +- Give each list item one paragraph only. Put a code block or a second + paragraph after the list, because the formatters add an empty line + between all items of a list that has one. + +## Comments + +- A comment explains the declaration that it belongs to: the contract, + invariants, and non-obvious reasons. Do not repeat what the code shows + or describe unrelated caller behavior. +- Update or remove a comment when its code changes. +- Write prose comments as continuous paragraphs, with one space between + words and sentences. Do not use semicolons in prose. +- Reflow each changed comment paragraph with `bin/reflow`. Do not reflow + by hand or with your own editor configuration. +- Give `bin/reflow` the first and the last line of the paragraph only. + Lists, examples, and directives in a comment keep their line breaks. + +This example reflows the comment on lines 12 to 15: + +```sh +bin/reflow main.go 12 15 +``` diff --git a/.agents/skills/go-code/SKILL.md b/.agents/skills/go-code/SKILL.md new file mode 100644 index 0000000..7de0a40 --- /dev/null +++ b/.agents/skills/go-code/SKILL.md @@ -0,0 +1,83 @@ +--- +name: go-code +description: Go style rules. Use before you write or review Go code. +--- + +# Go code + +## Packages and files + +- Keep Go code in one root `main` package. Do not add `internal/`, + `cmd/`, or one-file packages until the user asks for them. +- Keep `main.go` limited to the entry point. Name other files for their + function. Do not create `main_test.go`. +- A file carries its own context. Keep each struct and all of its + methods in one file. Give a separate responsibility its own type. +- Put entry points first, then each struct's methods as a group, then + standalone utility functions. Do not put a utility function between + the methods of one type. +- In each group, order functions by first use in the file. + +## Entry point and context + +- `main` calls `run()` and handles its returned error. Do not use + `log.Fatal`. +- `run()` creates the SIGINT/SIGTERM signal context and defers its + cancellation. Pass that context through operations and let cleanup + finish before `main` exits. +- Use `ctx` for a context and `cancel` for its cancellation function. + Call `defer cancel()` immediately after you create the context. +- Reuse `ctx` when a child context replaces its parent. Use a different + name only when both contexts stay in use. Return `ctx.Err()` directly. + +## Errors + +- Handle every returned error. +- Reuse an existing `err` for later operations in the same scope. Use a + different variable only to keep or join errors. +- Use `errors.Is` and `errors.As` to examine errors. Do not compare an + error directly with a sentinel, because errors can be wrapped. +- When a function has more than one error path, wrap each returned error + with the context of its operation. A function with one error path can + return the error directly. +- When deferred cleanup can fail, use a named `err` result and join the + cleanup error with `errors.Join`. + +## Style + +- Keep function signatures on one line. Write function bodies on more + than one line, also for anonymous functions, deferred cleanup, and + test callbacks. +- Do not use short `if` initializers. +- Use a one-letter receiver name for each method. +- Put each field of a keyed struct literal on its own line when you set + more than one field. +- Put an empty line between logical blocks, also after multiline + declarations, assignments, literals, and calls. Keep an operation with + its immediate error or result check. Keep a short declaration with its + immediate use. +- Use GNU-style command flags: two dashes for long flags, such as + `--cwd`, and one dash only for one-letter flags, such as `-h`. +- Prefer the Go standard library. Prefer Go over cgo. +- Do not add compile-time interface assertions. Prove compatibility + through actual assignments and calls. +- Use `// nolint: name` for lint directives. + +## Dependencies + +- After a change to `go.mod` or `go.sum`, update `vendorHash` in + `nix/package.nix`. While the module has no dependencies, `vendorHash` + is `null`. + +## Tests + +- Add tests for meaningful behavior. Do not add tests that only repeat + implementation details. +- Use `testify/require` for assertions. +- Name a table-driven case slice `testCases` and its range variable + `tc`. A subtest name describes the behavior, not a generic outcome or + input. +- In a test file, put constants immediately after the imports, then the + tests, then supporting types, methods, and helpers. +- Supply valid required dependencies in tests. Do not make a dependency + optional only to make a test simpler. diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..2b7a412 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 0000000..17f3d70 --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,94 @@ +#!/bin/sh +# Rejects a commit message that breaks the rules in AGENTS.md: a subject of 50 +# characters or fewer without a final period, one blank line, a body, body lines +# of 72 characters or fewer, and real line breaks instead of "\n". Prose +# paragraphs fill each line before wrapping. Formatted blocks keep their line +# breaks. A body line without spaces, such as a URL, may be longer. +set -eu + +file=$1 +subject=$(sed -n 1p "$file") +case $subject in + "Merge "* | "Revert "* | "fixup! "* | "squash! "*) exit 0 ;; +esac + +fail() { + printf 'commit-msg: %s\n' "$1" "write the message to a file and commit with git commit -F." >&2 + exit 1 +} + +if grep -q '\\n' "$file"; then + fail 'the message has a literal "\n"; use real line breaks' +fi + +[ ${#subject} -le 50 ] || fail "the subject has ${#subject} characters; the limit is 50" + +case $subject in + *.) fail "the subject ends with a period" ;; +esac + +[ -z "$(sed -n 2p "$file")" ] || fail "the second line must be blank" + +sed -n '3,$p' "$file" | grep -q '[^[:space:]]' || fail "the message has no body" + +long=$(sed -n '3,$p' "$file" | awk 'length($0) > 72 && $0 ~ / / { print NR + 2; exit }') +[ -z "$long" ] || fail "line $long is longer than 72 characters" + +short=$(awk ' + function check_paragraph( i, word) { + if (!formatted && !bad_line) { + for (i = 1; i < count; i++) { + word = lines[i + 1] + sub(/[[:space:]].*$/, "", word) + if (length(lines[i]) + 1 + length(word) <= 72) { + bad_line = numbers[i] + break + } + } + } + + count = 0 + formatted = 0 + } + + NR <= 2 { next } + + fence != "" { + if ($0 ~ /^(`{3,}|~{3,})[[:space:]]*$/ && substr($0, 1, 1) == fence) { + match($0, /^(`+|~+)/) + if (RLENGTH >= fence_length) fence = "" + } + next + } + + /^(`{3,}|~{3,})/ { + check_paragraph() + match($0, /^(`+|~+)/) + fence = substr($0, 1, 1) + fence_length = RLENGTH + next + } + + /^[[:space:]]*$/ { + check_paragraph() + next + } + + { + if ($0 ~ /^[[:space:]>#|]/ || + $0 ~ /^([-*+][[:space:]]|[0-9]+[.)][[:space:]])/ || + $0 ~ /^[-=*_]{3,}$/ || + $0 ~ /^[[:alnum:]][[:alnum:]-]*:[[:space:]]/) { + formatted = 1 + } + + lines[++count] = $0 + numbers[count] = NR + } + + END { + check_paragraph() + if (bad_line) print bad_line + } +' "$file") +[ -z "$short" ] || fail "unnecessary line break after line $short; reflow the paragraph to 72 characters" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba4f6ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.direnv/ +/result* +/onechat +/AGENTS.local.md diff --git a/.mdformat.toml b/.mdformat.toml new file mode 100644 index 0000000..01b9581 --- /dev/null +++ b/.mdformat.toml @@ -0,0 +1,2 @@ +wrap = 72 +number = true diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..07a8f57 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,60 @@ +# Agent guide + +Read this file in full. It holds the rules for every task. + +## References + +- `README.rst`: project scope, setup, and commands. +- `AGENTS.local.md`: the preferences of one person, machine, or account. + Git ignores it. Read it when it exists. When it conflicts with this + guide, this guide wins. + +## Skills + +Read the `SKILL.md` of a skill before you start the task that it covers: + +| Task | Skill | +| ----------------------------------------------- | ------------------------ | +| Write or change Go code or Go tests | `.agents/skills/go-code` | +| Write or change documentation or prose comments | `.agents/skills/docs` | +| Format and check the repository | `.agents/skills/check` | +| Make any commit | `.agents/skills/commit` | + +## Rules + +- Use ASD-STE100 Simplified Technical English for communication with the + user, documentation, and code comments. +- Every file describes the current state only. Do not write history in + files: no dates of changes, no "was", "previously", or "no longer", + and no changelogs or work logs. Git records what changed and why. +- Keep the implementation small. Add a feature, dependency, or + integration only when a task requires it. +- Change only what the task asks for. Do not change unrelated code to + match a style rule. +- Nix is the reference environment. The pinned flake supplies the Go + toolchain, the tools, and every dependency. Keep Nix code in `nix/`. + Do not install host dependencies or add a dependency that works only + outside Nix. +- Keep secrets and local runtime files out of Git and Nix sources. +- Do not reveal infrastructure in files, code comments, commit messages, + or issues. This includes host, server, and domain names, IP addresses, + ports, network devices, user names, paths outside the repository, and + deploy steps. Values from `AGENTS.local.md` or the local machine are + private too. +- Use a neutral placeholder, such as `example.com` or `HOST`, or + describe the condition instead. Write a real value only when the user + explicitly allows that value for the current task. +- Change `AGENTS.md` and `.agents/` only when the user asks. +- Put personal, machine, or account preferences in `AGENTS.local.md`, + never in a tracked file. + +## Decisions + +Stop and ask the user before you act when: + +- the task can be read in more than one way; +- two or more reasonable solutions give different results; +- the task conflicts with this guide or a skill. + +Ask one short question. Give the options and your recommendation. +Continue only with work that the answer cannot change. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..ce4696e --- /dev/null +++ b/README.rst @@ -0,0 +1,38 @@ +######### + OneChat +######### + +OneChat is a Go program. It prints its name. It is in early development. +Linux is supported. + +************* + Development +************* + +Nix supplies the Go toolchain and all tools. Enter the development shell +with direnv: + +.. code:: console + + direnv allow + +Or enter it directly: + +.. code:: console + + nix develop + +Build and run: + +.. code:: console + + nix build + ./result/bin/onechat + +Format and check all files: + +.. code:: console + + bin/format + bin/check + nix flake check diff --git a/bin/check b/bin/check new file mode 100755 index 0000000..b8071dc --- /dev/null +++ b/bin/check @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +# Lists the tracked and the new, not ignored files that match a pattern. +files() { + git ls-files --cached --others --exclude-standard -- "$@" +} + +unformatted=$(gofumpt -l .) +if [ -n "$unformatted" ]; then + printf 'gofumpt: unformatted files:\n%s\n' "$unformatted" >&2 + exit 1 +fi + +go vet ./... +golangci-lint run +go test -race ./... +files '*.nix' | xargs nixfmt --check +files '*.rst' | xargs rstfmt --check +files '*.md' | xargs mdformat --check +shellcheck bin/check bin/format bin/reflow .githooks/commit-msg diff --git a/bin/format b/bin/format new file mode 100755 index 0000000..512aca4 --- /dev/null +++ b/bin/format @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +# Lists the tracked and the new, not ignored files that match a pattern. +files() { + git ls-files --cached --others --exclude-standard -- "$@" +} + +gofumpt -w . +files '*.nix' | xargs nixfmt +files '*.rst' | xargs rstfmt +files '*.md' | xargs mdformat diff --git a/bin/reflow b/bin/reflow new file mode 100755 index 0000000..2f7575f --- /dev/null +++ b/bin/reflow @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Reflows the prose comment on lines FIRST to LAST of FILE. It uses the +# Vim from the Nix shell without a user configuration, so the result is +# the same on every machine. The filetype plugin of Vim supplies the +# comment markers. +set -euo pipefail + +if [ $# -ne 3 ]; then + echo 'usage: bin/reflow FILE FIRST LAST' >&2 + exit 2 +fi + +file=$1 +first=$2 +last=$3 + +vim -u NONE -i NONE -N -n -es \ + -c 'filetype plugin on' \ + -c "edit $file" \ + -c 'set textwidth=80 nojoinspaces formatoptions=cq' \ + -c "${first},${last}s/\\S\\zs \\+/ /ge" \ + -c "normal! ${first}GV${last}Ggq" \ + -c 'wq' diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..ab653e4 --- /dev/null +++ b/flake.lock @@ -0,0 +1,43 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1786599213, + "narHash": "sha256-yNJd40f11EzXBjSByCB7IPpeFFAdeoSKKM67dGkfFoU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0e251e24a4f24e036a084b6b4b2d2491af4167f4", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1689347949, + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default-linux", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b15cd5b --- /dev/null +++ b/flake.nix @@ -0,0 +1,43 @@ +{ + description = "OneChat"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + systems.url = "github:nix-systems/default-linux"; + }; + + outputs = + { + self, + nixpkgs, + systems, + }: + let + forEachSystem = nixpkgs.lib.genAttrs (import systems); + in + { + packages = forEachSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + onechat = pkgs.callPackage ./nix/package.nix { src = self; }; + in + { + inherit onechat; + default = onechat; + } + ); + + devShells = forEachSystem (system: { + default = import ./nix/devshell.nix { + pkgs = nixpkgs.legacyPackages.${system}; + }; + }); + + checks = forEachSystem (system: { + package = self.packages.${system}.default; + }); + + formatter = forEachSystem (system: nixpkgs.legacyPackages.${system}.nixfmt); + }; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ca9050b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module onechat + +go 1.26.5 diff --git a/main.go b/main.go new file mode 100644 index 0000000..271a91a --- /dev/null +++ b/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" +) + +func main() { + err := run() + if err != nil { + fmt.Fprintln(os.Stderr, "onechat:", err) + os.Exit(1) + } +} + +func run() error { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + _, err := fmt.Println("onechat") + if err != nil { + return err + } + + return ctx.Err() +} diff --git a/nix/devshell.nix b/nix/devshell.nix new file mode 100644 index 0000000..9ec0209 --- /dev/null +++ b/nix/devshell.nix @@ -0,0 +1,25 @@ +{ pkgs }: +pkgs.mkShell { + packages = with pkgs; [ + git + go + gofumpt + golangci-lint + gopls + nixfmt + (python3.withPackages (p: [ + p.mdformat + p.mdformat-gfm + p.mdformat-frontmatter + ])) + rstfmt + shellcheck + vim + ]; + + GOTOOLCHAIN = "local"; + + shellHook = '' + git config core.hooksPath .githooks 2>/dev/null || true + ''; +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 0000000..7b6623b --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,26 @@ +{ + lib, + buildGoModule, + src, +}: +buildGoModule { + pname = "onechat"; + version = "0.1.0"; + inherit src; + + subPackages = [ "." ]; + vendorHash = null; + + doInstallCheck = true; + installCheckPhase = '' + runHook preInstallCheck + test "$("$out/bin/onechat")" = onechat + runHook postInstallCheck + ''; + + meta = { + description = "OneChat"; + mainProgram = "onechat"; + platforms = lib.platforms.linux; + }; +}