From: Anderson A. Giacomolli Date: Tue, 10 Nov 2026 18:36:03 +0000 (-0300) Subject: Add Nix-built Go program and agent rules X-Git-Url: https://git.giacomolli.uk/onehouse/commitdiff_plain/7ef0ef316c8765dc42f704c26bf57eba28808def Add Nix-built Go program and agent rules OneHouse starts as a program that prints its name, with the rules and tools for all later work. The program gives the Nix package a binary that the install check can run, so the build pipeline is tested before the first feature. The agent guide and its skills hold the rules for Go code, documentation, checks, commits, and issues. The scripts in bin/ format and check the repository with the tools from the pinned flake, and the commit hook rejects a message that breaks the message rules. --- 7ef0ef316c8765dc42f704c26bf57eba28808def diff --git a/.agents/skills/check/SKILL.md b/.agents/skills/check/SKILL.md new file mode 100644 index 0000000..b09ec50 --- /dev/null +++ b/.agents/skills/check/SKILL.md @@ -0,0 +1,33 @@ +--- +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`. Its golangci-lint config keeps the + standard linters and adds more linters, such as `errorlint`, + `gocritic`, `testifylint`, `revive`, `wrapcheck`, `nlreturn`, and + `wsl_v5`. The settings in `.golangci.yml` match the Go rules. +3. Run `git add` on new files, then run `nix build` and + `nix flake check`. The package is Linux-only, so run both on Linux + before a change to Go or Nix code is complete. +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. Review the final diff against the rules. Passing tools do not replace + this review. + - Read `AGENTS.md` and the `SKILL.md` of each skill of the task again + from disk now. Do not work from memory of an earlier read. + - Go through the diff one rule at a time, and for each rule look at + every changed line that it covers. For Go code, check each section + of `go-code`, also for the tests. + - Fix each finding, then run steps 1 to 4 again. + +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..38007b4 --- /dev/null +++ b/.agents/skills/commit/SKILL.md @@ -0,0 +1,63 @@ +--- +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 `Issue:` + trailer with the git-bug ID of that issue. Add it with + `git commit --trailer`. Never use a closing keyword such as "Fixes" or + "Closes". +- Include a stable archive URL when a change comes from a mailing-list + or forum discussion. +- Do not add an AI tool as co-author. Do not add attribution trailers. + +## Example + +``` +Print the program name + +OneHouse 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..fd600f9 --- /dev/null +++ b/.agents/skills/docs/SKILL.md @@ -0,0 +1,101 @@ +--- +name: docs +description: Docs and comment rules. Use before you write any prose. +--- + +# Documentation and comments + +Change documentation and comments in the same change as the behavior, +interface, or constraint that they describe. + +## Documentation + +- `README.rst` is the entry point. It says what the project is and how + to build, run, and check it. Keep it short. +- `doc/` holds one document for each subject: a contract, an + integration, or a device. Add to the document of the subject before + you add a new document. +- Use reStructuredText. Use Markdown only for files that a tool requires + by name, such as `AGENTS.md` and `SKILL.md`. +- Follow + [Google's timeless documentation guide](https://developers.google.com/style/timeless-documentation). + Describe what the checked-in code does. Do not use labels that depend + on time, such as "new", "now", "currently", and "previously". +- Follow + [Google's tense guide](https://developers.google.com/style/tense). Use + the present tense for general behavior. Use the future tense only when + the timing of an action requires it, not for planned features. +- Keep previous designs, development history, roadmaps, and future work + in issues. +- State exact support and limits instead of labels such as "prototype". + Keep implemented, tested, and unverified behavior apart. +- Keep safety requirements and known limits explicit. For example, say + when emulator tests do not verify compatibility with the physical + device. +- Keep compatibility and old-schema details when the code or the + migration tests require them. They describe an active contract, not + history. +- Do not remove a warning or claim support only to remove historical + wording. Check the implementation and the tests before you change a + claim. +- Compare claims, examples, paths, and links with the implementation and + the tests. Make sure that examples use supported interfaces and + contain no secrets. + +## 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, non-obvious reasons, and required compatibility behavior. + Do not repeat what the code shows or describe unrelated caller + behavior. +- State input constraints, results, errors, and concurrency or resource + ownership rules when a caller needs them. +- Keep a comment next to the code that it describes. 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 +``` + +## Go comments + +- Follow [Go Doc Comments](https://go.dev/doc/comment). Write complete + sentences. Start a declaration comment with the name of the symbol. +- Do not write a package comment. +- Document each exported declaration and its contract. + +## Swift comments + +These rules apply to the iOS app in `ios/`. They follow the +[Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) +and Apple's +[symbol documentation guide](https://developer.apple.com/documentation/xcode/writing-symbol-documentation-in-your-source-files). + +- Use `///` for a documentation comment and `//` for an implementation + comment. +- Write a documentation comment for each type and for each member whose + use is not clear from its name. +- Start a documentation comment with a summary that ends with a period. + For a function, say what it does and what it returns. For a type or a + property, say what it is. The summary does not start with the symbol + name, and it can be a sentence fragment. +- Add `- Parameter`, `- Returns`, or `- Throws` only when the summary + does not make them clear. +- Describe the behavior of the code and the server API that it uses. Do + not write which server features are missing or will come. Write what + the app does without them, and put the missing feature in an issue. +- Text that the app shows is not a code comment. diff --git a/.agents/skills/go-code/SKILL.md b/.agents/skills/go-code/SKILL.md new file mode 100644 index 0000000..7811d2b --- /dev/null +++ b/.agents/skills/go-code/SKILL.md @@ -0,0 +1,147 @@ +--- +name: go-code +description: Go style rules. Use before you write or review Go code. +--- + +# Go code + +## Packages and files + +- Put commands in `cmd/` and shared packages at the repository root + beside `cmd/`. Do not add an `internal` package. +- Give each package one clear responsibility and a small public API. + Keep implementation types and helpers private. +- Keep composition and dispatch in the main service type. Put distinct + work in focused private types or standalone functions with explicit + inputs. Do not spread the methods of the service type across files to + make a file shorter. +- 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 the file that declares the struct. Give a separate + responsibility its own type. +- Name a file that holds a struct after that struct, in snake_case. Use + the platform suffix for a platform-specific file. When platforms share + the struct, put the platform details in standalone helpers. +- Write all the letters of an initialism in one case. `revive` checks + common initialisms. +- Put entry points first, then constructors with a `new` or `New` + prefix, 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. +- Group related constants in one `const (...)` block and related + variables in one `var (...)` block. Put an empty line between entries + that have comments. + +## Entry point and context + +- `main` creates the SIGINT/SIGTERM signal context and defers its + cancellation. Pass that context through operations and let cleanup + finish before `main` exits. Do not use `log.Fatal`. +- 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. +- Derive each context from the context of the caller. When work must + finish even after the caller is cancelled, such as a final write, use + `context.WithoutCancel` of the caller's context. Use + `context.Background()` only where no caller context exists: in `main`, + and in methods that follow `io.Closer`. +- Name a duration that limits one operation after that operation, and + end the name with `Timeout`, so that the name says what it limits + without its comment. Define it in the file that applies it, not in the + file of another type. + +## Errors + +- Handle every returned error. Do not discard an error with the blank + identifier. `errcheck` checks this rule. +- 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. + `errorlint` checks comparisons and type assertions on errors. +- Handle a documented sentinel error explicitly when it does not show a + failure. +- 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. `wrapcheck` checks package and interface + calls, with the exceptions in `.golangci.yml`. Review local calls and + the number of error paths by hand. +- When deferred cleanup can fail, use a named `err` result and join the + cleanup error with `errors.Join`. + +## Logs and cancellation + +- Log with `log/slog`. Write the message as a short event in plain + words, such as "receive failed". Put names, IDs, counts, and durations + in attributes with short keys. Put the cause in `err`. Do not format + values into the message. +- Use info for normal events, warn for a failure that is retried or + loses nothing, and error for a failure that loses data or stops a + part. +- Cancellation of a service or request context is an expected stop, not + a failure. Do not log it as an error, and do not hide all errors only + because the context is canceled. A joined error with an independent + failure stays a failure. +- Use the worker or request context for this check. A device request + that reaches its own timeout while its worker is active stays a + failure. +- A client that cancels an HTTP request does not get a server-error + response for that cancellation. +- Never log secrets or tokens. + +## 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. +- Use field names in struct literals. Put each field on its own line + when you set more than one field. This includes the cases of a + table-driven test. +- Put an empty line between logical blocks, such as two independent `if` + statements. Put an empty line before and after loops, switches, + selects, standalone blocks, and multiline declarations, assignments, + literals, and calls, when there is an adjacent statement. These + boundaries also apply when a loop immediately uses the declaration + before it. +- Keep an operation with its immediate error or result check. Keep a + short declaration with its immediate use, except at the boundaries + above. `wsl_v5` and `nlreturn` check part of these rules. Review the + remaining logical blocks and immediate-use groups by hand. +- Group imports in this order: standard library, external dependencies, + project imports. `gci` checks the groups. +- 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 the Nix + package. + +## Tests + +- Add tests for behavior changes. Do not add tests that only repeat + implementation details, and do not add a test file without behavior + worth a test. +- Put tests in a file that matches the tested source file. +- Use `testify/require` for assertions. `testifylint` checks the + assertion forms. +- 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. Keep each + supporting struct with its methods. +- Keep a helper that only one test file uses in that file. Put a helper + that more than one test file uses in a file without tests. Name that + file for what it holds, and end the name with `_helper_test.go`. +- Supply valid required dependencies in tests. Do not make a dependency + optional or add a nil fallback only to make a test simpler. diff --git a/.agents/skills/issues/SKILL.md b/.agents/skills/issues/SKILL.md new file mode 100644 index 0000000..4227d00 --- /dev/null +++ b/.agents/skills/issues/SKILL.md @@ -0,0 +1,73 @@ +--- +name: issues +description: Read and write issues with git-bug. Use for any issue work. +--- + +# Issues + +Issues live in git-bug, inside the Git repository, not in files. The +`overview` issue lists the phases and the open decisions. Run the +commands in the Nix development shell. + +## Read + +- List the open issues with `git bug bug status:open`. +- Read an issue and all its comments with `git bug bug show ID` before + you start work on it. + +## Write + +- Give the title and the body with `-t` and `-m`. Do not use `-F`: it + drops every line that starts with `#`, so Markdown headings are lost. +- Write the title like a commit subject: 50 characters or fewer, in the + imperative mood, with no final period. +- Write the body in Markdown. State the result for the user, the scope, + and how to check it. +- Format each body and comment with `mdformat -` in the repository, so + that it uses `.mdformat.toml`. mdformat joins trailer lines into one + line, so format the text first and add the trailers after it. +- Quote a long text as a blockquote below the paragraph or list, not as + inline code. mdformat cannot wrap inline code. +- Add a comment with `git bug bug comment new ID -m TEXT` for progress + that matters: decisions made with the user, measurements, and checks + that could not run. +- When a question to the user is still open at the end of a session, add + it as a comment with the options and your recommendation. Add the + answer as a comment when it comes. +- Give the reason with each decision, so that a later agent does not + undo it. +- Close an issue with `git bug bug status close ID` only when its check + passes. +- Label a phase `phase`, an open question `decision`, and the plan + `overview`, with `git bug bug label new ID LABEL`. + +## Relations + +git-bug has no links between issues. Write them as trailers in the last +paragraph of the body, one per line, as in a commit message: + +- `Parent: ID` names the issue that this issue belongs to. A child has + one parent. +- `Depends-on: ID, ID` names the issues that must be done first. +- A parent lists the IDs of its children in its body. + +`git interpret-trailers --parse` reads the trailers of a body. + +Find the issues that relate to an issue with `git bug bug ID`. The +search finds every issue that mentions the ID, so read the trailers of +each match. Do not search for more than one word: git-bug finds issues +that match any of the words, and it reads `Parent:` as a query +qualifier. Nothing checks the trailers. Before you close a parent, make +sure that its children are closed. + +Push the issues with `git bug push` when the repository has a remote. + +This example creates an issue: + +```sh +body=$(mdformat - < body.md) +git bug bug new --non-interactive -t 'Keep camera buffers through slow disk writes' \ + -m "$body + +Parent: 180a674" +``` 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..9593921 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.direnv/ +/.devenv/ +/result* +/AGENTS.local.md diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..7e2dc79 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,92 @@ +version: "2" + +linters: + default: standard + enable: + - errname + - errorlint + - gocritic + - godot + - makezero + - misspell + - nlreturn + - predeclared + - revive + - testifylint + - unconvert + - wrapcheck + - wsl_v5 + settings: + errcheck: + # go-code: Do not discard an error with the blank identifier. + check-type-assertions: true + check-blank: true + errorlint: + errorf: true + asserts: true + comparison: true + godot: + scope: declarations + capital: true + govet: + enable-all: true + disable: + - fieldalignment + misspell: + locale: US + wsl_v5: + # go-code: Keep an operation with its immediate error check. + enable: + - err + - after-block + - after-decl + - after-defer + - after-expr + - after-go + # go-code: Keep a short declaration with its immediate use. + allow-first-in-block: true + allow-whole-block: true + cuddle-max-statements: 2 + disable: + # go-code: Keep a short declaration with its immediate assignment use. + - assign + # go-code: Keep a short declaration, its operation, and its error check + # together. + - if + # go-code: Keep a declared cleanup with its immediate defer. + - defer + # go-code: Keep a short declaration with its immediate append use. + - append + # go-code: Keep a short declaration with its immediate worker setup. + - go + nlreturn: + # go-code: Keep a short declaration with its immediate use. + block-size: 2 + wrapcheck: + # go-code: Return ctx.Err() directly. + extra-ignore-sigs: + - "(context.Context).Err() error" + exclusions: + rules: + # docs: Do not write a package comment. + - linters: [revive] + text: "package-comments" + +formatters: + enable: + - gofumpt + - gci + settings: + gofumpt: + module-path: onehouse + gci: + # go-code: Group imports as standard library, external, project. + sections: + - standard + - default + - prefix(onehouse) + custom-order: true + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 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..3a05a4f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,68 @@ +# Agent guide + +Read this file in full. It holds the rules for every task. + +## References + +- `README.rst`: project scope, setup, and commands. +- `doc/`: one document for each subject. Read the document of a subject + before you change its code. +- `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` | +| Read or write an issue | `.agents/skills/issues` | + +## 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. + Issues hold plans and future work. +- 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. Xcode on macOS is the only exception. +- Write only ASCII characters in files, commit messages, and issues. Use + `<-` and `->` for arrows, `-` for dashes, and straight quotes. Use + other characters only in text that an application shows to the user, + in names in a house configuration, and in test data that needs them. +- 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..84983f4 --- /dev/null +++ b/README.rst @@ -0,0 +1,29 @@ +########## + OneHouse +########## + +OneHouse is one program that controls a whole house. Linux is supported. + +************* + Development +************* + +Nix supplies the toolchain and every tool. Enter the development shell: + +.. code:: console + + nix develop + +With direnv, the shell loads when you enter the checkout. The shell sets +the Git hooks path to ``.githooks``. + +Format all files, then run the checks: + +.. code:: console + + bin/format + bin/check + nix build + nix flake check + +``AGENTS.md`` holds the rules for work in this repository. diff --git a/bin/check b/bin/check new file mode 100755 index 0000000..8449f47 --- /dev/null +++ b/bin/check @@ -0,0 +1,21 @@ +#!/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=$(golangci-lint fmt --diff) +if [ -n "$unformatted" ]; then + printf 'golangci-lint fmt: unformatted files:\n%s\n' "$unformatted" >&2 + exit 1 +fi + +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..9cdb3e3 --- /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 -- "$@" +} + +golangci-lint fmt +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/cmd/onehouse/main.go b/cmd/onehouse/main.go new file mode 100644 index 0000000..d38fba4 --- /dev/null +++ b/cmd/onehouse/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + "os" +) + +func main() { + _, err := fmt.Println("onehouse") + if err != nil { + os.Exit(1) + } +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..9f35b56 --- /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": 1791244623, + "narHash": "sha256-uuanbkFk8a313Zfrxdcz8Us/yQtaxNnKJtX8v0ZPpd8=", + "owner": "nix-systems", + "repo": "default", + "rev": "d36eeed142f88b5a636f2e3856a53c61654cf29b", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d81ba0e --- /dev/null +++ b/flake.nix @@ -0,0 +1,53 @@ +{ + description = "OneHouse"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + systems.url = "github:nix-systems/default"; + }; + + outputs = + { + self, + nixpkgs, + systems, + }: + let + allSystems = import systems; + # The program runs on Linux. The development shell also supports macOS. + linuxSystems = builtins.filter (nixpkgs.lib.hasSuffix "-linux") allSystems; + forEachSystem = nixpkgs.lib.genAttrs allSystems; + forEachLinuxSystem = nixpkgs.lib.genAttrs linuxSystems; + date = self.lastModifiedDate; + revision = self.shortRev or self.dirtyShortRev or "unknown"; + version = "${builtins.substring 0 4 date}-${builtins.substring 4 2 date}-${builtins.substring 6 2 date}-${revision}"; + in + { + packages = forEachLinuxSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + onehouse = pkgs.callPackage ./nix/package.nix { + src = self; + inherit version; + }; + in + { + inherit onehouse; + default = onehouse; + } + ); + + devShells = forEachSystem (system: { + default = import ./nix/devshell.nix { + pkgs = nixpkgs.legacyPackages.${system}; + }; + }); + + checks = forEachLinuxSystem (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..9e58e09 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module onehouse + +go 1.26.5 diff --git a/nix/devshell.nix b/nix/devshell.nix new file mode 100644 index 0000000..fdf65e9 --- /dev/null +++ b/nix/devshell.nix @@ -0,0 +1,26 @@ +{ pkgs }: +pkgs.mkShell { + packages = with pkgs; [ + git + git-bug + 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..6b37eeb --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,26 @@ +{ + lib, + buildGoModule, + src, + version, +}: +buildGoModule { + pname = "onehouse"; + inherit src version; + + subPackages = [ "cmd/onehouse" ]; + vendorHash = null; + + doInstallCheck = true; + installCheckPhase = '' + runHook preInstallCheck + [[ "$("$out/bin/onehouse")" == "onehouse" ]] + runHook postInstallCheck + ''; + + meta = { + description = "OneHouse"; + mainProgram = "onehouse"; + platforms = lib.platforms.linux; + }; +}