--- /dev/null
+---
+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.
--- /dev/null
+---
+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.
+```
--- /dev/null
+---
+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
+```
--- /dev/null
+---
+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.
--- /dev/null
+../.agents/skills
\ No newline at end of file
--- /dev/null
+#!/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"
--- /dev/null
+/.direnv/
+/result*
+/onechat
+/AGENTS.local.md
--- /dev/null
+wrap = 72
+number = true
--- /dev/null
+# 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.
--- /dev/null
+AGENTS.md
\ No newline at end of file
--- /dev/null
+#########
+ 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
--- /dev/null
+#!/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
--- /dev/null
+#!/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
--- /dev/null
+#!/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'
--- /dev/null
+{
+ "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
+}
--- /dev/null
+{
+ 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);
+ };
+}
--- /dev/null
+module onechat
+
+go 1.26.5
--- /dev/null
+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()
+}
--- /dev/null
+{ 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
+ '';
+}
--- /dev/null
+{
+ 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;
+ };
+}