diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 508ebdd..82a7519 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -11,6 +11,7 @@ ".clawpatch/", ".codex/", "AGENTS.md", - "CLAUDE.md" + "CLAUDE.md", + "archive/**" ] } diff --git a/AGENTS.md b/AGENTS.md index 814cfa1..4380e78 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,46 @@ Before taking any action, read @README.md for project context. Use `askpplx` for real-time web search via Perplexity. Verify external facts—documentation, API behavior, library versions, best practices—before acting on them. A lookup costs far less than debugging hallucinated code. Run `npx -y askpplx --help` if unsure of the available options. +# Rule: Safe Command Execution + +## Store commands in arrays, not strings + +When Bash expands a string variable, quotes inside become literal characters and whitespace triggers word splitting: + +```bash +# BAD: quotes are literal, spaces split words +CMD="echo \"hello world\"" +$CMD # outputs: "hello world" (with literal quotes) + +# GOOD: array preserves argument boundaries +CMD=(echo "hello world") +"${CMD[@]}" # outputs: hello world +``` + +## Never interpolate variables into shell strings + +Variables interpolated into shell strings — `sh -c`, `bash -c`, `eval`, `ssh host` — are reparsed by the shell. Characters like `$(...)`, backticks, or `;` in the value execute as code, a classic injection vector: + +```bash +# BAD: if VAR contains $(malicious), it executes +sh -c "$VAR --write" + +# GOOD: direct execution, no shell interpretation +"${CMD[@]}" --write + +# GOOD: with xargs, execute the array directly +find . -name '*.js' -print0 | xargs -0 "${CMD[@]}" --write -- +``` + +When you need shell features (pipes, redirects), use the `exec "$@"` pattern to pass arguments as positional parameters instead of interpolating them: + +```bash +# GOOD: arguments passed as $@, not interpolated into the string +xargs -0 sh -c 'exec "$@"' _ "${CMD[@]}" --write -- +``` + +The `_` occupies `$0` (the script name), leaving `$@` for the command and arguments. Any string works as the placeholder; `_` is conventional. + # Rule: Avoid Leaky Abstractions Design interfaces around what callers need, not how the system works internally. An abstraction is leaky when using it correctly requires knowledge of underlying storage, infrastructure, or error behavior. Keep signatures consistent, return domain types instead of backend artifacts, and inject infrastructure dependencies through constructors rather than method parameters. @@ -331,7 +371,7 @@ For Node.js 22.6–22.17, use `--experimental-strip-types`. Older versions requi # Rule: Use `repoq` for Repository Queries -Use `repoq` for reading repository state instead of piping `git` or the forge CLI through `awk`/`jq`/`grep`. Each command handles edge cases (detached HEAD, unborn branches, missing auth) and returns validated JSON. Use raw `git` for commit/push/merge, and the repo's forge CLI for forge-side mutations (PRs, issues, releases) — `gh` for GitHub or `fgj` for Forgejo, per the detected provider. Run `npx -y repoq --help` if unsure of the available subcommands. +Use `repoq` for reading repository state instead of piping `git` or the forge CLI through `awk`/`jq`/`grep`. Each command handles edge cases (detached HEAD, unborn branches, missing auth) and returns validated JSON. Use raw `git` for commit/push/merge, and the repo's forge CLI for forge-side mutations (PRs, issues, releases) — `gh` for GitHub or `fgj` for Forgejo, per the detected provider. Run `npx -y repoq@latest --help` if unsure of the available subcommands; the explicit tag prevents `npx` from reusing a stale cached release. # Rule: Discriminated Unions diff --git a/README.md b/README.md index a7ec96d..c6eb477 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # sync-rules +> **⚠️ Retired (2026-07-29).** This tool is superseded by **j4k-align** +> (`j4k/align` on `code.j4k.dev`), which now generates every agent instruction +> file. Do not install or run `sync-rules` — a run would overwrite +> j4k-align-generated files with this repository's stale corpus rendering. +> See [`RETIREMENT.md`](./RETIREMENT.md) for the full retirement record. + A CLI tool to synchronize AI coding assistant rule files between a central repository and multiple projects. [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) diff --git a/RETIREMENT.md b/RETIREMENT.md new file mode 100644 index 0000000..1fc5c6b --- /dev/null +++ b/RETIREMENT.md @@ -0,0 +1,23 @@ +# sync-rules is retired + +Retired 2026-07-29. Agent instruction files (`AGENTS.md`, `CLAUDE.md`, the +workstation globals) are now generated by **j4k-align** (`j4k/align` on +`code.j4k.dev`) via `j4k-align agents --fix`, from the rule corpus in that +repository's `rules/` directory. The full migration record is +`docs/sync-rules-retirement-plan.md` in `j4k/align`. + +- Deployed version read from the npm registry at cutover: **5.11.5**. +- The final `config.json` (verbatim from + `~/Library/Preferences/sync-rules/config.json` at retirement) is archived at + [`archive/config.json`](./archive/config.json). +- The last sync-rules renders of the five workstation global files are archived + under [`archive/global-targets/`](./archive/global-targets/). These are the + bytes live on the workstation immediately before the j4k-align regeneration + (the 2026-07-27 corpus re-sync render); the Phase-0 baseline tar named by the + retirement plan was no longer present on the retiring machine, and its + per-file hashes remain recorded in j4k-align's `docs/pre-cutover-hashes.json`. + +Phase 5 of the retirement plan deprecates the npm package on the registry +(never unpublishing it) and archives this repository once this record is on +`main`. Do not run the CLI — a run would overwrite j4k-align-generated files +with this repository's stale corpus rendering. diff --git a/archive/config.json b/archive/config.json new file mode 100644 index 0000000..a53f914 --- /dev/null +++ b/archive/config.json @@ -0,0 +1,570 @@ +{ + "global": ["ai-coding-workflow/*.md"], + "globalOverrides": { + "codex": ["codex/*.md"], + "copilot": ["copilot/*.md"], + "claude": ["claude/*.md"] + }, + "projects": [ + { + "path": "/Users/jercik/Developer/j4k/ia-lcp-books", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/media-preservation", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/review", + "rules": [ + "000-personal-header/*.md", + "references-directory/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/align", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/tropkod", + "rules": [ + "000-personal-header/*.md", + "references-directory/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k-oss/tropkod-client", + "rules": [ + "000-personal-header/*.md", + "references-directory/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/claude-code-reconstruction-workbench", + "rules": [ + "000-vc-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/smart-home", + "rules": [ + "000-vc-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/git-arc", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/axkit/apps/axconsole", + "rules": [ + "000-personal-header/*.md", + "005-nextjs/*.md", + "react/*.md", + "react-project-structure/*.md", + "tailwind/*.md", + "tailwindcss-react-aria-components/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/repoq", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/miniread", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "rust/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/miniread-lab", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/miniread-node", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/eslint-config-axkit", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/axkit", + "rules": [ + "000-axkit-header/*.md", + "core/*.md", + "ai-coding-agents-source-code/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/gh-feedback", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/askpplx", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/fta-check", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/worktree-add", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/worktree-remove", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/echarts-agent", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "react/*.md", + "tailwind/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/cluster", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "ansible/*.md", + "containers/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/stealth-scrapper", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md", + "containers/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/setup-atlas", + "rules": [ + "000-personal-header/*.md", + "references-directory/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/sync-rules", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/perplexity-agent-mcp", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/cc-restoration", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "rust/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/i2pd-exporter", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "rust/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/conventional-commit-msg", + "rules": [ + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/deluge-to-qbittorrent", + "rules": [ + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/jercik/i2pd-webconsole-exporter", + "rules": [ + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "rust/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/imax-praha", + "rules": [ + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/kap-staking", + "rules": [ + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md", + "react/*.md", + "tailwind/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/moviebox", + "rules": [ + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/moviefiles", + "rules": [ + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md", + "react/*.md", + "tailwind/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/propolis-v0.8.0", + "rules": [ + "core/*.md", + "askpplx/*.md", + "tropkod/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/qbittorrent-tools", + "rules": [ + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/relief-ui", + "rules": [ + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md", + "react/*.md", + "tailwind/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/tracker-rules", + "rules": [ + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/ui", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md", + "react/*.md", + "storybook/*.md", + "tailwind/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/oxlint-config-j4k", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/semantic-release-github-output", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/dynamic-tools", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/agent-research", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k/agent", + "rules": [ + "000-personal-header/*.md", + "references-directory/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md", + "nodejs/*.md", + "typescript/*.md" + ] + }, + { + "path": "/Users/jercik/Developer/j4k-oss/agent-skills", + "rules": [ + "000-personal-header/*.md", + "core/*.md", + "askpplx/*.md", + "tropkod/*.md", + "repoq/*.md" + ] + } + ] +} diff --git a/archive/global-targets/claude-CLAUDE.md b/archive/global-targets/claude-CLAUDE.md new file mode 100644 index 0000000..e4349c4 --- /dev/null +++ b/archive/global-targets/claude-CLAUDE.md @@ -0,0 +1,404 @@ +# Rule: 1Password Commit Signing + +This machine signs git commits via 1Password. Any signing error during `git commit` — 1Password socket errors, "failed to sign the data", "fatal: failed to write commit object" — usually means 1Password is locked. Ask the user to unlock 1Password, then retry the commit. + +# Rule: `AGENTS.md` Is Generated — Edit the Source + +`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md` and the like are generated by the `sync-rules` CLI. The next `sync-rules` run overwrites direct edits. + +To change what an agent reads, edit one of two sources: + +- **Rule content** in `~/Developer/j4k/setup-atlas/rules//*.md` — reword an existing file or add a new one under the appropriate category. +- **Which rules apply to which repo** in `/Users/jercik/Library/Preferences/sync-rules/config.json` — add or remove a glob (e.g., `"nodejs/*.md"`) under a `projects[].path` entry, or under `global` / `globalOverrides` for cross-repo defaults. + +Run `sync-rules` after either edit. + +# Rule: Automatic Repository Alignment Scope + +When the user asks to align or audit "all my repositories", that means the repository checkouts directly under `~/Developer/jercik/`, `~/Developer/j4k/`, and `~/Developer/j4k-oss/` — nothing outside those three directories. + +# Rule: Canonical Repository Checkout Layout + +Place every owned repository under `~/Developer//`, using the remote owner and repository slugs rather than a locally invented prefix: + +- GitHub `Jercik/example` → `~/Developer/jercik/example` +- GitHub `validationcloud/example` → `~/Developer/validationcloud/example` +- Forgejo `j4k/example` → `~/Developer/j4k/example` +- Forgejo `j4k-oss/example` → `~/Developer/j4k-oss/example` + +Use the remote repository name verbatim. A historical local prefix is not part of the name: Forgejo `j4k/align` belongs at `~/Developer/j4k/align`, never `~/Developer/j4k-align`. + +Git worktrees live in the same owner directory as the main checkout, with the branch name appended to the directory name as `-`: a worktree for branch `foo` of `~/Developer/j4k/align` goes at `~/Developer/j4k/align-foo`. Create worktrees with `worktree-add ` from inside the repository — it places the new checkout at that canonical path automatically, copies useful local files, and installs dependencies. + +Before cloning, resolve the forge and owner/repository slug, create the owner directory, and pass the canonical destination explicitly. The account-aware `gh` shim selects the ValidationCloud account and SSH key for `validationcloud/*` and `lukasz-jercinski-vc/*` targets, and the personal account otherwise. Forgejo clones use the canonical tailnet SSH transport and an explicit destination. `code.tail.j4k.dev` is reachable only while the machine is connected to the Tailscale network — a clone or fetch that hangs or can't connect usually means the tailnet is down, not that the key or remote is wrong; check the Tailscale connection before debugging SSH. + +Third-party source remains under `~/Developer/third-party/` and is never reorganized by owner. Do not infer an owner for a Git root with no remote or for a plain local directory; leave it in place until the user classifies it. + +# Rule: You Share This Workspace + +Other agents and the user may have uncommitted WIP in the working tree, and new changes can appear mid-session. Don't assume unexpected state came from your edits, and don't stash, overwrite, or commit work you didn't make — even stash-then-pop can confuse another agent whose tree state shifts underfoot. + +If you need an isolated tree, ask the user about creating a git worktree and move your changes there. + +# Rule: Conventional Commits + +Write every git commit message and pull request title in Conventional Commits format (`type: subject`). + +Before authoring a PR title and body, load the `pr-writing-style` skill if it is installed — it owns the prose: title wording, body shape, what gets cut. + +# Rule: Create PRs on the Repo's Forge (No Shell Expansion) + +Detect the forge first — [the forge detection rule](./forge-provider-detection.md) owns the how — and use the matching PR tool: `gh` for GitHub, `fgj` for Forgejo. Don't reach for `gh` reflexively; it can't open a PR on Forgejo. + +The shared trap is shell expansion of multi-line Markdown bodies. A double-quoted body string lets the shell expand backticks and `$...` before the CLI sees it, mangling code blocks and variable references. Pass the body so the shell never scans it. + +**GitHub (`gh`)** — `--body-file` with a single-quoted heredoc (`'EOF'` disables all expansion): + +```bash +gh pr create --title "docs: clarify example" --body-file - <<'EOF' +Clarifies that the `..${sep}` check doesn't block `..foo/bar.txt`. +EOF +``` + +**Forge (`fgj`)** — `fgj pr create` takes only `-b` (no `--body-file`), so write the body with a single-quoted heredoc, then pass it by command substitution. The file's bytes become the argument verbatim; the shell does not re-scan them for expansion: + +```bash +forge=$(mktemp) +npx -y repoq@latest forge --json > "$forge" +apiHost=$(jq -r '.apiHost' "$forge") # e.g. code.j4k.dev, codeberg.org +slug=$(jq -r '.slug' "$forge") +body=$(mktemp) +cat > "$body" <<'EOF' +Clarifies that the `..${sep}` check doesn't block `..foo/bar.txt`. +EOF +fgj --hostname "$apiHost" pr create -R "$slug" \ + --title "docs: clarify example" --base main --head my-branch \ + -b "$(cat "$body")" +rm -f "$body" "$forge" +``` + +When running from the target repo with the j4k build configured, `fgj` can auto-detect the repo and API host, including `git_hosts` aliases for SSH-only transport hosts. For scripts, cross-repo work, or commands launched outside the target repo, still pass `--hostname "$apiHost"` and `-R "$slug"` from `repoq forge --json`; explicit flags remove cwd and remote-alias ambiguity. `fgj pr edit -F ` accepts a body file if you'd rather create then edit. + +Verify with the matching CLI: `gh pr view --json title,body,url` or `fgj --hostname "$apiHost" pr view -R "$slug" --json`. + +# Rule: Doc-Comments and Example Snippets Are Documentation + +Audit JSDoc/docstrings and example code in source with the same rigor as markdown. Example calls must type-check against the current signature; a required `T | undefined` field omitted from an example is drift even though it "looks" fine. + +# Rule: Encode Cross-Repo Config Learnings in j4k-align + +`j4k-align` (in `~/Developer/j4k/align/`) audits and aligns repo settings on either forge, automation inputs, workflows, rulesets (GitHub) / branch protection (Forgejo), and managed files across every repo. Its checks and templates are the source of truth for cross-repo configuration — on-disk state drifts; the audit defines what "correct" means. + +j4k-align detects the forge per repo from its git origin and audits and aligns config across both GitHub (via `gh`) and the self-hosted Forgejo instance (`code.j4k.dev`, via `fgj`) end to end — there is no Forge gap to work around. + +When you hit a config issue in one repo — a misbehaving lint rule, a missing tsconfig field, a workflow that breaks on a new dependency — ask: **"Will this come up again in another repo?"** If yes, the fix belongs in `j4k-align`. Patching one repo lets the same trap resurface elsewhere; encoding it in the audit catches it everywhere. + +## Traits, triggers, and the extension loop + +Repos are classified by **traits** — labels like `pnpm-package`, `react`, `nextjs`, `external-references`, `private-oci-publish` defined in `src/alignment/schemas.ts`. Each trait is assigned by a **trigger** — a signal from the repo (file presence, `package.json` dependency, `.gitmodules` content, registry config). Traits gate checks and template selection. + +Touchpoints when extending the audit: + +- Trigger: `src/traits/signals.ts` +- Trait assignment: `src/traits/build-traits.ts` (enum in `src/alignment/schemas.ts`) +- Trait-gated check: `src/cli/verify/check-*.ts`, wired in `src/cli/verify/run-checks.ts` +- Fix implementation: `src/checks/*.ts`, wired in `src/cli/fix/local-project-configs/fix-local-tool-configs.ts` +- Templates: `templates/`, with `TraitSelection` guards in `src/resolve-template-files.selections.ts` + +**Fix the signal, not the symptom.** If a repo should have a trait but doesn't — or has one it shouldn't — the trigger is wrong. Repair `src/traits/signals.ts` rather than hardcoding the trait downstream. Verify with `j4k-align --repo owner/name`, then `--fix` to confirm remediation. + +## Forbid the trap, not just the current case + +When you hit a known footgun — a setting that silently breaks types, a flag that disables a guarantee, a path pattern that traps on edge cases — add a check that rejects that value across every applicable trait, not one that only repairs the current repo. Forbid the specific bad shape, name the check clearly, and record the failure mode it prevents inside the check so the reasoning travels with the code. + +## Scope + +j4k-align governs cross-repo configuration: build, lint, format, tsconfig, workflows, rulesets (GitHub) / branch protection (Forgejo), repo settings on either forge, automation secrets and variables, and managed files (git hooks, `release.config.mjs`, etc.). Project-specific business code, product schema, and feature behavior stay in their repos. + +## Anti-patterns + +- `eslint-disable`, `// @ts-ignore`, or `.gitignore` entries that silence a warning other repos will hit identically. +- Hand-editing `.github/workflows/*.yml` or `.forgejo/workflows/*.yml` when the source-of-truth template lives in `templates/`. +- Diverging a per-repo `tsconfig.json` to dodge a check instead of fixing the check. +- Hardcoding a trait or skipping a check when the real bug is in the trigger. + +This is a specialized application of _Fix the Foundation First_ — the foundation here is the trait/trigger/check/fix pipeline. + +# Rule: `eslint-config-axkit` Is Deprecated + +Superseded by oxlint with `@j4k/oxlint-config`; it cannot run under the fleet's `typescript@^7` pin, which breaks `typescript-eslint`. Never add it to a project — when touching a repo that still lints through it, migrate to oxlint instead. + +# Rule: Use the j4k Custom Build of `fgj` + +The j4k fork at [`codeberg.org/jercik/fgj`](https://codeberg.org/jercik/fgj) ships features not yet in upstream `romaintb/fgj` — `fgj pr review` with inline comments, `fgj pr review resolve`/`unresolve` for conversation resolution (against the j4k Forgejo fork's resolution API), `fgj pr checks` for a PR's combined CI status, the `fgj pr list --base`/`--head` server-side filters, org-scoped `fgj actions` secrets/variables (with pipeable secret input), and `git_hosts` aliases for remotes whose SSH host differs from the Forgejo API host. Features leave this list as they merge: the generic `fgj api` passthrough and `fgj repo view --json` landed upstream in `v0.5.0` (2026-07). Other rules here lean on these commands, so prefer this build over the stock Homebrew one. + +**Check the build, not the version number.** The `-j4k.N` suffix on `fgj --version` is the durable tell; a bare upstream version (e.g. `0.4.0`) means stock. Gate on the suffix, which survives version bumps: + +```bash +fgj --version | grep -q j4k || echo "stock fgj — install the j4k build" +``` + +The suffix only proves the fork lineage, not any one feature — features accrete across `-j4k.N` releases, so an older fork build passes the gate while missing a newer verb. When a workflow depends on a specific command, gate on the parent's subcommand listing. An exit-code probe (`fgj pr review resolve --help`) false-passes on the build it exists to catch: cobra reads the unknown word as a positional argument and answers `--help` with the parent's help, exit `0`. The listing discriminates — scoped to the `Available Commands:` block, so an indented prose or example line that happens to start with a verb name can't satisfy it — and checks every verb the workflow needs, since the accretion argument above applies verb by verb: + +```bash +help=$(fgj pr review --help) \ + || { echo "fgj pr review --help failed — is fgj installed?" >&2; exit 1; } +verbs=$(awk '/^Available Commands:/{f=1;next} /^[^[:space:]]/{f=0} f' <<<"$help") +for v in resolve unresolve; do + grep -qE "^[[:space:]]+$v([[:space:]]|$)" <<<"$verbs" \ + || { echo "fgj build has no $v verb — install the newest v*-j4k.* release" >&2; exit 1; } +done +``` + +The `--help` failure branch is the missing-or-broken-binary case — cobra answers `--help` with exit `0` even on a build with no `pr review` at all (the parent-fallback above), so a nonzero exit means the command never ran. Every other shape falls through to the per-verb check, whose message names the right remedy for all of them: the parent `pr` help of a stock or pre-`pr review` fork build, and even a `pr review` leaf command whose help prints no `Available Commands:` block at all — released j4k builds always print it (`pr review` shipped with `list` and `comments` already registered), but the probe no longer leans on that history to route the failure. The `exit 1` makes the snippet a hard gate for scripts; a workflow that can degrade instead — like the feedback-processing skill, which skips only the resolve step on a failed probe — runs the same probe and branches on its exit status rather than dying. + +Install the newest `v*-j4k.*` tag from the [releases page](https://codeberg.org/jercik/fgj/releases) — `scripts/install.sh` is checksum-verified for CI and local machines; in a Dockerfile, pull the `linux_{amd64,arm64}.tar.gz` release asset directly. Binaries are static and CGO-free, so they run anywhere (alpine, distroless, scratch). + +For Forge repos whose `origin` uses a transport-only host such as `code.tail.j4k.dev`, configure the API host with a Git alias instead of passing the SSH host as `--hostname`: + +```bash +fgj auth login --hostname code.j4k.dev --git-host code.tail.j4k.dev +``` + +Features are upstreamed one small PR at a time. Until a feature lands upstream and ships in a stock release, the j4k build is the source of truth. + +# Rule: Fix the Foundation First + +When you hit an issue likely to recur, stop and solve the underlying problem rather than working around it. The upfront investment pays for itself every time the issue would have resurfaced. + +For example: if you don't know how an API works and will need it repeatedly, don't guess at endpoints — build a tool that fetches and displays its docs. If a manual step keeps recurring, automate it. If knowledge is missing, capture it in a script, command, or doc so it's available next time. + +The key question: _"Will this come up again?"_ If yes, fix the root cause now. + +# Rule: Detect the Forge — GitHub (`gh`) vs Forgejo (`fgj`) + +Repos are mid-migration across two forges, so **never assume `gh`** — it only speaks GitHub's API. Detect the provider from `origin` before any forge operation (PRs, issues, CI/checks, releases, repo metadata, branch protection) and use the matching CLI. + +- **GitHub** — `github.com`, CLI `gh`, names untouched (`Jercik/j4k-cluster`). +- **The Forge** — self-hosted Forgejo at `code.j4k.dev`, CLI [`fgj`](https://codeberg.org/romaintb/fgj). Repos live under `j4k/` and drop the `j4k-` affix (`j4k-cluster` → `j4k/cluster`; an already-unprefixed repo like `setup-atlas` keeps its name). + +## Detection + +Run `npx -y repoq@latest forge --json`. The explicit tag prevents `npx` from reusing a stale cached release. It normalizes every URL shape (scp-like, `ssh://`, `https://`) and handles the `j4k-` affix that a hand-rolled `sed` gets wrong, returning `provider` (`github`/`forgejo`/`unknown`), `cli`, `slug`, `hostnameFlag`, `apiHost`, `apiBase`, plus `host`/`webHost`/`sshHost`/`owner`/`repo`. Reach for `apiHost` when passing a host to `fgj` — `hostnameFlag` is a two-word string that zsh, which does not word-split expansions, hands `fgj` as a single argument. Treat `unknown` as a hard stop. + +## Driving `fgj` + +`fgj` mirrors `gh`'s verbs (`pr`, `issue`, `release`, `repo`, `label`, `milestone`, with `--json` on the read verbs). For a PR's combined CI status, use `fgj pr checks ` (add `--json` for machine-readable output; exit codes match `gh pr checks` — `0` success/warning/skipped/no-checks, `1` failure/error, `8` pending). CI run history lives under `fgj actions run list|view|watch`; for step logs use `fgj actions run view --log` (also `--log-failed`, `-j `) — don't decompress the on-disk `actions_log/*.zst` chunks. + +When running inside the target repo, the j4k build of `fgj` can auto-detect the repo and API host. If `origin` uses a transport-only SSH host such as `ssh://git@code.tail.j4k.dev:2222/…`, configure that hostname as a `git_hosts` alias under the real API host (`code.j4k.dev`) and do not pass the SSH host as `--hostname`. + +For scripts, cross-repo commands, or work launched outside the target repo, use `repoq forge --json` and pass the returned host and slug explicitly: `fgj --hostname "$apiHost" -R "$slug" ...`. Explicit flags are still the most deterministic shape when cwd or remote config might not describe the target repo. The leading position is convention, not requirement — `--hostname` is a root persistent flag that cobra parses before or after the subcommand, so a trailing literal `--hostname ` (the `fgj auth` examples) is equally correct; the hazard the convention guards is the two-word `$hostnameFlag` expansion, which breaks at any position. One nuance, not an exception: the `auth` verbs declare a local `--hostname` that shadows the root flag, but cobra hands a leading flag to that local flag too, so both positions keep working — the difference is only that the shadowed root value never reaches the config fallback, and `auth login` has no fallback at all: pass its `--hostname` explicitly (either position) or it prompts interactively. + +## Beyond `fgj`'s verbs: `fgj api` + +Some operations have no dedicated verb. Reach for the generic `fgj api ` passthrough — it reuses the configured auth and resolved API host. The passthrough started in [the j4k custom build of `fgj`](./fgj-custom-build.md), which the other `fgj` rules already assume, and landed upstream in `v0.5.0` (2026-07); on an older stock `fgj` without it, fall back to `curl "$apiBase/"` with a token from `fgj auth token --hostname "$apiHost"` — capture it first and require it non-empty (`token=$(fgj auth token --hostname "$apiHost")`; an empty substitution would send an unauthenticated request whose `401` reads as an instance fault instead of a missing token), then pass it as a `curl` config on stdin — `printf 'header = "Authorization: token %s"\n' "$token" | curl -fsS --config - "$apiBase/"` — not as an `-H` argv header any process on the machine could read from the process table. + +- **Branch protection** (the Forge's replacement for GitHub "rulesets"): `/repos/{owner}/{repo}/branch_protections[/{name}]`. +- **A PR's combined CI status**: use the dedicated `fgj pr checks ` command instead — not the raw passthrough (add `--json` for machine-readable output; exit codes match `gh pr checks` — `0` success/warning/skipped/no-checks, `1` failure/error, `8` pending). +- **Combined commit status for an arbitrary commit not tied to a PR**: `/repos/{owner}/{repo}/commits/{sha}/status`. +- **Repo metadata as JSON**: `/repos/{owner}/{repo}` — or just `fgj repo view --json` (upstream since `v0.5.0`, alongside the passthrough). + +## Inspecting all Forgejo PR feedback + +Forgejo splits PR discussion across three API surfaces: issue comments at `/repos/{owner}/{repo}/issues/{number}/comments`, review summaries at `/repos/{owner}/{repo}/pulls/{number}/reviews`, and inline comments under each review at `/repos/{owner}/{repo}/pulls/{number}/reviews/{review-id}/comments`. Reading one surface misses the others: a review's summary body often carries only a count ("Found 2 medium issues") while the findings live in its inline comments, and responses land as issue comments. Web URLs anchor inline comments as `#issuecomment-`, but the API exposes them only under `/pulls/{number}/reviews/{review-id}/comments`, never in `/issues/{number}/comments`. + +Before handing off a PR, prove the review cycle is complete for the current head: + +1. Wait for the `PR Review` action run whose `commit_sha` equals `git rev-parse HEAD` (the run appears asynchronously — retry an empty result), then wait until every job in that run is terminal. A run-level failure or a terminal `fgj pr checks` does not mean all reviewer jobs finished. +2. Read the expected reviewer set from that run's jobs endpoint, not from workflow files in the checkout — `pull_request_target` executes the base branch's workflow. +3. Require every expected non-skipped reviewer job to have a submitted review whose `commit_id` is that head, and `fgj pr checks ` to report the same head in `.sha`. A reviewer job that exits without publishing a verdict blocks handoff — retry it. +4. Sweep all three surfaces (pagination rules below) and repeat until two consecutive sweeps return identical totals and records — a pending review can become visible without changing `X-Total-Count`. +5. After any follow-up push, redo the whole check against the new head; earlier reviews say nothing about the updated commit. + +Pagination on Forgejo 16.0.0: + +- **Reviews**: fetch `?limit=&page=N` for every page through `ceil(X-Total-Count / limit)`, taking the limit from `/settings/api` (`max_response_items`, 50 on code.j4k.dev). Don't stop at an underfilled page — Forgejo filters other users' pending reviews after database pagination, so a short page can precede later submitted reviews. The endpoint sends neither `Link` nor `X-HasMore`, so `fgj api --paginate` cannot prove completeness. Keep `PENDING` reviews in the sweep snapshot but don't treat their unpublished bodies or comments as feedback. A persistent gap between `X-Total-Count` and the deduplicated visible reviews is another user's unpublished draft — record it as a diagnostic, not a handoff blocker. +- **Issue comments**: one fetch returns everything — the endpoint ignores `page` and `limit`. Require the distinct returned IDs to match its `X-Total-Count`. + +`gh-feedback summary --json` aggregates issue comments, the inline review comments it reaches, and their reactions and responses — useful, but not proof of completeness: it omits review summary bodies and its Forgejo pager stops on an underfilled review page. Use it alongside the sweep, not instead of it. + +When processing or handing off a PR — not during a read-only audit — address every actionable finding and acknowledge the response on the PR before reporting completion. + +**Resolving conversations depends on the instance.** The j4k Forgejo fork adds a REST conversation-resolve API; stock Forgejo — upstream and codeberg.org, through 16.0.x — has none. Detect the capability from the `version` endpoint (`fgj --hostname "$apiHost" api version` — explicit host, or a probe run outside the target repo answers for `fgj`'s default host and reads that instance's capability instead; or `curl -fsS "$apiBase/version"` — `-f` so an HTTP error exits nonzero instead of handing the substring test an error body): the version string contains `-j4k` (live: `16.0.1-j4k.1+gitea-1.22.0`) on the fork, and `code.j4k.dev` qualifies — test for the substring, since build metadata (`+gitea-…`) trails the marker. The substring proves fork lineage, not the endpoint: an older `-j4k` server that predates the resolution route passes the test and `404`s on the first resolve — treat that like an unreadable version, skipping resolution for the pass and reporting it. A version without `-j4k` is stock Forgejo (e.g. codeberg.org's `16.0.0-dev-626-32363b81+gitea-1.22.0`); an unreadable version — `404`, network error, a body that isn't JSON with a `.version` string — blocks only resolution, not the feedback pass: process feedback normally, skip the resolve step, and report the failed probe rather than treating it as a stock verdict. This is the server's version — don't infer it from `fgj --version`, which reports the CLI build, a separate j4k fork that talks to stock servers just fine; that gate is effectively always true on this machine while the target instance may well be stock. Comment-minimize exists on neither build — Forgejo has no minimize concept at all. + +On a `-j4k` instance, `gh-feedback` v3.3.0+ owns native transitions for items it tracks; older builds use the feedback skill's qualified direct fallback. Raw-only workflows resolve with `fgj --hostname "$apiHost" pr review resolve -R "$slug"` and reopen with `… unresolve` (j4k `fgj` build `v0.5.0-j4k.4`+) — the explicit `-R` matters as much here as anywhere, since an omitted repo falls back to cwd detection. Any comment id in the thread works: the command lists the PR's reviews and their comments itself, walks to the thread's anchor, names the anchor it targeted, and reports the updated `resolver` (`--json` returns the updated anchor comment). Gate on the parent's subcommand listing per the j4k `fgj` build rule — on an older j4k build without the verb the call dies with cobra's `accepts 1 arg(s), received 3`, a usage error that means the verb is missing, not that you mistyped, and the fix is installing the newest build — not scripting a raw fallback; the REST path behind the verb goes deliberately unnamed here so it can't accrete one. Argument mistakes fail before any write with their own messages — `failed to list reviews` for a wrong PR number or slug, `not an inline review comment` for a review summary, issue comment, or foreign id (they aren't conversations) — and that second message also covers an inline id deleted since your sweep listed it, because the command re-lists every review comment before writing: re-run the sweep before reading it as your own mistyped id. A `404` from the resolve call itself means the server has no resolution API — stock Forgejo, or a `-j4k` server predating the route; the only deleted-id `404` is a comment vanishing in the instant between the command's own listing and its write. Token auth, same gate as other PR writes; success returns the updated anchor, whose `resolver` reflects real DB state and stays the original resolver on an idempotent re-resolve. + +**Verification still needs the conversation partition.** The resolve endpoint writes `resolver` to exactly the comment it is handed, and the UI reads a conversation's resolved state only from its anchor — that is why the command walks to the anchor before writing, and why the completeness sweep must read each anchor's `resolver` rather than any reply's. The API exposes no threading field, so derive conversations from the sweep's inline comments: a conversation is the code comments sharing a `path`, a side with its display line, and a `pull_request_review_id` — replies join the review they answer, while comments that different reviews leave at the same line are separate conversations, each resolved on its own. The side is whichever of `position` (new side) / `original_position` (old side) is nonzero — the unused side reports `0`, and old-side and new-side comments at the same number are distinct conversations; both at `0` is the stored line-`0` edge (the server keeps one signed line and reports it on a single side), which groups like any other value — same review, same path, line `0`, one conversation — and the display line is that number plus `extra_lines_count` — upstream Forgejo API since `v16.0.0`, where multi-line comments landed, not a fork field: Forgejo buckets a multi-line comment at the _end_ of its range, so a comment spanning 55–60 and a single-line comment at 60 from the same review are one conversation, and grouping by the raw `position` pair alone splits them and mis-picks the anchor. The anchor is the conversation's earliest comment (`created_at`, ties by lowest `id`). This partition is the one the API listings and the Conversations tab render; the sweep's `position` is the line as of the comment's own commit, while the diff view re-blames comments to the current head and can merge same-review buckets after lines move — don't expect it to mirror the derivation. `gh-feedback` derives this same native partition when reading or changing resolver state, but its feedback items still thread by reply markers: an item id is therefore not necessarily the anchor, and two findings one review left at the same display line are separate items sharing one conversation whose single resolve state — the anchor's, per the write-where-handed endpoint above, so a sibling root comment can carry a stray `resolver` from a direct write without changing what renders — speaks for both. Confirm the anchor's `resolver` is set after resolving — the per-review comment listings the sweep already fetches return it on every comment — and read it first to know the current state before unresolving. + +On a stock instance no resolve endpoint exists — the review `dismissals` endpoint dismisses a review's verdict, not a conversation, and the web "Resolve conversation" route authenticates by browser session cookie only, so a token POST 303-redirects to `/user/login` and resolves nothing. Track "done" by reaction there instead of pretending to resolve. + +# Rule: Generated Agent-File Drift Is Expected + +Automated tooling rewrites tracked agent instruction files (`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md`) whenever shared rules change, so these files may sit modified for long stretches. + +When they are the only modifications, treat the repository as clean — don't stash, revert, or delete the changes, and don't commit them on their own. Include them in the next substantive commit or pull request instead. + +# Rule: Local Third-Party Checkouts + +Before searching the web or relying on memory for an external library, check `~/Developer/third-party/` — it holds dozens of third-party repos cloned locally for reference. The source is faster and more authoritative than any secondary description. + +Run `ls ~/Developer/third-party/` to see what's available, then explore as with any local code. Don't clone new repos into this directory unsolicited — the user curates it. + +# Rule: Target Bash 3.2 + +Unless the user specifies otherwise, write shell scripts for bash 3.2 — the default `/bin/bash` on macOS. + +When using `printf`, do not pass a literal string starting with `-` as the format string. Use `printf '%s\n' "$value"` or `echo` so Bash 3.2 does not parse the marker as an option. + +Backslash line continuation inside a `for … in` word list across multiple lines is fragile in Bash 3.2 and can fail with "syntax error near unexpected token `done`". Collect the patterns in an array (`globs=(g1 g2 g3)`) and iterate with a nested loop, or put all patterns on a single line. + +# Rule: No Backwards Compatibility + +Prioritize the best shape of the current codebase over backwards compatibility. When something changes, change it completely — rename the function, delete the old flag, stop reading the old config path. Let the code reflect what it _is_, not what it _was_; version control remembers what was there. + +Remove every form of backwards-compatibility ballast: + +- Aliases and re-exports keeping old import paths or type names working +- Fallback reads from old config locations or env var names +- Renamed-but-kept flags, options, or CLI arguments +- Feature flags gating already-shipped changes +- Underscore-prefixed "unused" variables kept to silence lint +- `// removed: …` or `// TODO: delete in v3` comments marking absent code +- Deprecated types re-exported from their old module +- Database migrations committed before the project has real users — reset the dev DB and iterate + +Compatibility code accumulates as dead weight: it obscures intent, inflates surface area, and forces every future reader to reason about states that no longer exist. + +## Database migrations + +Committed migrations are permanent — every schema tweak becomes a file that travels with the project forever, even if the only "old" schema lived for an afternoon in a dev DB. Before a deployment holds data you can't discard, reset the dev DB and iterate on the schema directly; commit only the current state. A squashed clean initial schema is far cheaper to reason about than a chain of pre-release churn. + +Once real users have real data, the mode flips: every schema change becomes a migration, no exceptions. The cutover is "first deployment with persistent real data," not "first commit" or "first merged PR." The same reasoning extends to any on-disk or over-the-wire format you cannot discard — serialized caches, stored session blobs, published API shapes — iterate freely before the first real reader exists, then lock it down. + +# Rule: Access OrbStack Machines with `orb`, Not SSH + +To run a command inside an OrbStack Linux machine, use `orb -m -u ` (wrap in `bash -lc '…'` when you need a login shell, PATH, or pipes). The standing dev VM is `debian` with user `j4k`: + +```bash +orb -m debian -u j4k bash -lc 'node --version' +``` + +Don't reach for `ssh @.orb.local` — it authenticates by public key and fails with `Permission denied (publickey)` unless that user already has your key in `authorized_keys`. `orb` reuses OrbStack's host identity mapping, so it needs no key and works for any existing user on the machine. + +# Rule: Package Manager Execution + +How different package manager commands resolve binaries: + +| Command | Behavior | +| ----------------- | ----------------------------------------------------------------------- | +| `pnpm exec foo` | Runs from `./node_modules/.bin`; falls back to system PATH | +| `pnpx foo` | Always fetches from registry (uses dlx cache); ignores local installs | +| `npx foo` | Checks local `node_modules/.bin` → global → downloads from registry | +| `npx foo@version` | Resolves version, uses local if exact match exists, otherwise downloads | + +`pnpx` is an alias for `pnpm dlx`. + +# Rule: Prefer OrbStack Locally + +Use OrbStack as the local container and Linux VM runtime on macOS — not Docker Desktop, Colima, or a Podman machine. The `docker` and `docker compose` CLIs work unchanged; `orbctl` (aliased `orb`) creates and manages full Linux VMs. + +OrbStack has faster cold starts, lower idle CPU and memory, native macOS file sharing without bind-mount workarounds, and a single tool for both containers and VMs. Assume any container or VM workflow on this host runs through OrbStack. + +For ad hoc Linux VM testing, see the `orbstack-ad-hoc-vm` skill. + +# Rule: Prefer TypeScript Over Python + +When writing new code and the user states no language requirement, default to TypeScript. This yields to explicit user input: write Python when the user asks, when the task lives in a Python codebase, or when the ecosystem forces it (data science, ML, a Python-only library). + +# Rule: Project Skill Symlinks + +Keep the real skill in `.agents/skills//` and treat `.claude/skills/` as a symlink to it, not a second copy. Create the Claude entry with `ln -s ../../.agents/skills/ .claude/skills/` so both locations point at the same source of truth. + +# Rule: Runtime Tool Discovery + +When a workflow references a custom CLI — a local script or anything you may not already know — run ` --help` before first use. The help output is the authoritative source for subcommands, flags, and usage; rules only name the tool and rely on `--help` to teach you the rest at runtime. + +Compose these tools with pipes like any Unix CLI, and prefer machine-readable formats (`--porcelain`, `--json`) over parsing human-readable output. + +# Rule: Repository Scripts + +`scripts/` is the default home for the repository's operational tooling — automation, helpers, and one-offs that maintain the repo but aren't part of what it ships. + +**Look there first.** If `scripts/` exists, `ls scripts/` and `jq '.scripts' package.json` before writing anything new — extend what's there rather than forking it. + +**Put new scripts there by default.** Anything worth committing — release helpers, submodule updates, git hooks, recurring chores — goes in `scripts/`, wired through `package.json` so callers invoke it by name rather than remembering the path. + +# Rule: Set an Explicit Timeout for Long CI Waits + +Waiting on CI to finish — `gh run watch`, `fgj actions run watch`, or a poll loop over `gh run list --commit ` — routinely outlasts an agent shell tool's default command timeout (Claude Code's Bash tool defaults to 120s, raisable to 600s). Pass an explicit longer timeout (e.g. 420000 ms) to the command invocation, or the wait is killed mid-run and reports a false failure. + +# Rule: Sub-Agent Delegation + +Spawn sub-agents liberally. A sub-agent encapsulates a chunk of work behind a simple interface: briefed on the task, it gathers its own context, works autonomously, and hands back only the result — trust the process and engage with the outcome. The main agent's job is to frame tasks, dispatch, and synthesize results. + +**Delegate, move on, verify.** A task of many simple steps is prime delegation material — deploying an Ansible playbook and combing its verbose logs where nearly every task just reports ok, provisioning a throwaway OrbStack VM with Node, Docker, and Postgres installed, clicking through a multi-page web flow filling fields and pressing buttons, watching a CI run, applying a bulk mechanical edit. Hand it to a background sub-agent, move on to other work while it runs, and when it finishes, spawn a fresh sub-agent to confirm the work was done correctly — and for critical work, several adversarial reviewers, each attacking the result through a different lens. + +**Skills delegate too.** Instead of invoking a skill yourself, consider handing it to a sub-agent: name the skill and the inputs, and the sub-agent loads it, follows the workflow, and returns the result — the skill's full instruction set never enters the main context. `agent-browser` is the perfect shape for this: "log in to the site and check that such-and-such feature works" is a one-line instruction with a one-line answer, and everything in between — dozens of tool calls, failed selectors, retries — stays encapsulated in the sub-agent. Whether that fits is a per-skill call. + +# Rule: TSV Parsing + +`awk` splits on any whitespace by default, silently breaking on TSV values containing spaces. For tab-separated output (often `--porcelain` flags), set the delimiter explicitly: + +```bash +# BAD: prints "name" instead of "name with spaces" +printf 'id\tname with spaces\tstatus\n' | awk '{ print $2 }' + +# GOOD — pick one: +awk -F'\t' '{ print $2 }' +cut -f2 # cut defaults to tab +while IFS=$'\t' read -r a b c; do …; done +``` + +Empty fields are a second trap for the `read` form only: tab is IFS _whitespace_, so runs of tabs collapse into one delimiter and an empty middle field shifts every value after it — `awk -F'\t'` and `cut -f` are immune. When a field can be empty, prefer `awk`/`cut`, keep nullable fields last, or in zsh double the tab (`IFS=$'\t\t'`), which the manual defines as demoting it to a hard delimiter that preserves empty fields; bash has no doubled form — it silently ignores the doubling and shifts the fields anyway, so a bash-run copy of the zsh idiom reinstates the exact bug it exists to prevent, with no diagnostic. + +Not every `--porcelain` is TSV — `git worktree list --porcelain` is space-separated per line. Sample output before picking a delimiter. + +# Rule: Reach for Unix-Native Primitives Before Inventing Abstractions + +Use the OS as the first control plane. Before proposing a registry, supervisor, scheduler, logger, IPC layer, config store, or discovery protocol, check whether argv, environment variables, inherited file descriptors, filesystem paths, Unix-domain sockets, ports, signals, stdout/stderr, cron, systemd, or XDG paths already solve it. + +Two primitives cover almost everything: + +- **Handoff at fork/exec** — parent passes addresses to children via args, env vars, or inherited FDs (`SSH_AUTH_SOCK`, systemd socket activation). +- **Well-known names in a shared namespace** — filesystem paths and TCP/UDP ports (`/var/run/docker.sock`, port 22). The filesystem is the service directory. + +Common problems map directly: process discovery → socket at a conventional path or env var; IPC → Unix-domain socket, named pipe, or signal; supervision → systemd or another init; scheduling → cron or systemd timer; logging → stdout/stderr; config → XDG config dir. + +For Node apps needing a config, data, cache, log, or temp directory, default to [`env-paths`](https://github.com/sindresorhus/env-paths). It returns the right location per platform — XDG on Linux, `~/Library/...` on macOS, `%APPDATA%` on Windows — so you don't hand-roll `process.platform` branches that drift. Pass a namespace (`envPaths('my-app')`) and use the returned `config`, `data`, `cache`, `log`, `temp` paths directly. + +**Don't build a second control plane.** Reject the native primitive only when you can name the concrete property it cannot provide: distributed discovery across hosts, authorization the OS namespace cannot enforce, schema evolution for a long-lived wire format, multiplexing many streams over one transport, binary streaming with backpressure, or cross-platform targets where no equivalent primitive exists everywhere. + +Otherwise the native primitive wins. Designs that don't compose with pipes, signals, and conventional file locations pay a tax forever. + +# Rule: Use Native TypeScript Execution + +Use Node 24+ and run `.ts` files directly with `node script.ts`. Node strips types at runtime — no `tsx`, no `ts-node`, no `tsc` build step. + +Default to `.ts` over `.mjs` for new scripts and to `node` over `tsx` in `package.json`. + +# Rule: Adversarial Debate for Design Proposals + +The plausible survives review; only the attacked survives adoption. Before adopting a batch of design proposals from research or brainstorming — new audit/lint rules, org-wide standards, policy shapes — run each through an adversarial debate: an attacker briefed to kill it (a fresh-context agent, never the proposer critiquing itself), the author defending or conceding, and an independent judge ruling accept / accept-with-changes / reject. + +Debate earns its cost when proposals encode claims about external system behavior (tool semantics, config precedence, CI runner quirks) and when a wrong decision propagates into every repo it governs. Skip it when ground truth is one command away (run the command instead), when the choice is one-off and reversible, and when reviewing a diff — debate designs, not implementations. + +- **Argue from evidence, not priors.** Give every debater the relevant checkouts and read-only command access, and instruct them to reproduce claims. A reproduced failure is fatal; an argued one is an opinion. +- **Give the defense a concession valve.** The defender may revise mid-debate or concede fully; brief the judge that a well-defended dead idea still loses. +- **The revision is the product.** Apply the judge's required changes verbatim. If every proposal passes unchanged, the attackers were too weak — re-run them harsher. +- **Keep a rejection ledger.** Record each killed idea with a one-line cause of death so it isn't re-proposed. +- **Verify survivors before acting.** A verdict is still a hypothesis — check each survivor's load-bearing empirical claim directly; stale builds and version skew fool judges too. + +# Rule: Sub-Agent Model Selection + +Never launch a sub-agent with any `haiku` or `sonnet` model — use exactly two configurations, routed by the deliverable: + +- `opus` at `medium` — the executor, when the deliverable is facts or a completed task: running commands and test suites, searching a codebase for symbols or call sites, browsing and scraping pages, collecting evidence against given criteria, extracting data from files or docs, applying a specified change. Multi-step, tool-heavy, trial-and-error work belongs here — failed calls and adjusted arguments are churn the `medium` executor absorbs, handing back only the outcome. +- `fable` at `xhigh` — when the deliverable is a conclusion: reviewing work, synthesizing findings into a report, diagnosing a root cause, choosing an approach, weighing tradeoffs, designing a plan. + +"Find every `fetchUser` call site and list file:line" is executor work — the deliverable is locations the driver interprets. "Review this diff for bugs" is `fable` work — the deliverable is the judgment itself. + +Set the effort explicitly wherever it can be set (workflow `agent()` calls, agent definition frontmatter). Never raise the executor above `medium` — a task that seems to need more is delivering conclusions, so it belongs on `fable` — and never lower `fable` below `xhigh`. + +# Rule: Prompt Sub-Agents by Reference + +Prompt sub-agents by reference, not by paste. Give file paths, URLs, issue numbers, or skill names and tell the sub-agent what to read or invoke. A sub-agent is perfectly capable of retrieving the content itself, and letting it do so keeps that content from ever passing through the orchestrator's context. The orchestrator's budget then goes to what only it can do — analyzing the task and directing the work — not to relaying material that already lives elsewhere. diff --git a/archive/global-targets/codex-AGENTS.md b/archive/global-targets/codex-AGENTS.md new file mode 100644 index 0000000..6e70559 --- /dev/null +++ b/archive/global-targets/codex-AGENTS.md @@ -0,0 +1,388 @@ +# Rule: 1Password Commit Signing + +This machine signs git commits via 1Password. Any signing error during `git commit` — 1Password socket errors, "failed to sign the data", "fatal: failed to write commit object" — usually means 1Password is locked. Ask the user to unlock 1Password, then retry the commit. + +# Rule: `AGENTS.md` Is Generated — Edit the Source + +`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md` and the like are generated by the `sync-rules` CLI. The next `sync-rules` run overwrites direct edits. + +To change what an agent reads, edit one of two sources: + +- **Rule content** in `~/Developer/j4k/setup-atlas/rules//*.md` — reword an existing file or add a new one under the appropriate category. +- **Which rules apply to which repo** in `/Users/jercik/Library/Preferences/sync-rules/config.json` — add or remove a glob (e.g., `"nodejs/*.md"`) under a `projects[].path` entry, or under `global` / `globalOverrides` for cross-repo defaults. + +Run `sync-rules` after either edit. + +# Rule: Automatic Repository Alignment Scope + +When the user asks to align or audit "all my repositories", that means the repository checkouts directly under `~/Developer/jercik/`, `~/Developer/j4k/`, and `~/Developer/j4k-oss/` — nothing outside those three directories. + +# Rule: Canonical Repository Checkout Layout + +Place every owned repository under `~/Developer//`, using the remote owner and repository slugs rather than a locally invented prefix: + +- GitHub `Jercik/example` → `~/Developer/jercik/example` +- GitHub `validationcloud/example` → `~/Developer/validationcloud/example` +- Forgejo `j4k/example` → `~/Developer/j4k/example` +- Forgejo `j4k-oss/example` → `~/Developer/j4k-oss/example` + +Use the remote repository name verbatim. A historical local prefix is not part of the name: Forgejo `j4k/align` belongs at `~/Developer/j4k/align`, never `~/Developer/j4k-align`. + +Git worktrees live in the same owner directory as the main checkout, with the branch name appended to the directory name as `-`: a worktree for branch `foo` of `~/Developer/j4k/align` goes at `~/Developer/j4k/align-foo`. Create worktrees with `worktree-add ` from inside the repository — it places the new checkout at that canonical path automatically, copies useful local files, and installs dependencies. + +Before cloning, resolve the forge and owner/repository slug, create the owner directory, and pass the canonical destination explicitly. The account-aware `gh` shim selects the ValidationCloud account and SSH key for `validationcloud/*` and `lukasz-jercinski-vc/*` targets, and the personal account otherwise. Forgejo clones use the canonical tailnet SSH transport and an explicit destination. `code.tail.j4k.dev` is reachable only while the machine is connected to the Tailscale network — a clone or fetch that hangs or can't connect usually means the tailnet is down, not that the key or remote is wrong; check the Tailscale connection before debugging SSH. + +Third-party source remains under `~/Developer/third-party/` and is never reorganized by owner. Do not infer an owner for a Git root with no remote or for a plain local directory; leave it in place until the user classifies it. + +# Rule: You Share This Workspace + +Other agents and the user may have uncommitted WIP in the working tree, and new changes can appear mid-session. Don't assume unexpected state came from your edits, and don't stash, overwrite, or commit work you didn't make — even stash-then-pop can confuse another agent whose tree state shifts underfoot. + +If you need an isolated tree, ask the user about creating a git worktree and move your changes there. + +# Rule: Conventional Commits + +Write every git commit message and pull request title in Conventional Commits format (`type: subject`). + +Before authoring a PR title and body, load the `pr-writing-style` skill if it is installed — it owns the prose: title wording, body shape, what gets cut. + +# Rule: Create PRs on the Repo's Forge (No Shell Expansion) + +Detect the forge first — [the forge detection rule](./forge-provider-detection.md) owns the how — and use the matching PR tool: `gh` for GitHub, `fgj` for Forgejo. Don't reach for `gh` reflexively; it can't open a PR on Forgejo. + +The shared trap is shell expansion of multi-line Markdown bodies. A double-quoted body string lets the shell expand backticks and `$...` before the CLI sees it, mangling code blocks and variable references. Pass the body so the shell never scans it. + +**GitHub (`gh`)** — `--body-file` with a single-quoted heredoc (`'EOF'` disables all expansion): + +```bash +gh pr create --title "docs: clarify example" --body-file - <<'EOF' +Clarifies that the `..${sep}` check doesn't block `..foo/bar.txt`. +EOF +``` + +**Forge (`fgj`)** — `fgj pr create` takes only `-b` (no `--body-file`), so write the body with a single-quoted heredoc, then pass it by command substitution. The file's bytes become the argument verbatim; the shell does not re-scan them for expansion: + +```bash +forge=$(mktemp) +npx -y repoq@latest forge --json > "$forge" +apiHost=$(jq -r '.apiHost' "$forge") # e.g. code.j4k.dev, codeberg.org +slug=$(jq -r '.slug' "$forge") +body=$(mktemp) +cat > "$body" <<'EOF' +Clarifies that the `..${sep}` check doesn't block `..foo/bar.txt`. +EOF +fgj --hostname "$apiHost" pr create -R "$slug" \ + --title "docs: clarify example" --base main --head my-branch \ + -b "$(cat "$body")" +rm -f "$body" "$forge" +``` + +When running from the target repo with the j4k build configured, `fgj` can auto-detect the repo and API host, including `git_hosts` aliases for SSH-only transport hosts. For scripts, cross-repo work, or commands launched outside the target repo, still pass `--hostname "$apiHost"` and `-R "$slug"` from `repoq forge --json`; explicit flags remove cwd and remote-alias ambiguity. `fgj pr edit -F ` accepts a body file if you'd rather create then edit. + +Verify with the matching CLI: `gh pr view --json title,body,url` or `fgj --hostname "$apiHost" pr view -R "$slug" --json`. + +# Rule: Doc-Comments and Example Snippets Are Documentation + +Audit JSDoc/docstrings and example code in source with the same rigor as markdown. Example calls must type-check against the current signature; a required `T | undefined` field omitted from an example is drift even though it "looks" fine. + +# Rule: Encode Cross-Repo Config Learnings in j4k-align + +`j4k-align` (in `~/Developer/j4k/align/`) audits and aligns repo settings on either forge, automation inputs, workflows, rulesets (GitHub) / branch protection (Forgejo), and managed files across every repo. Its checks and templates are the source of truth for cross-repo configuration — on-disk state drifts; the audit defines what "correct" means. + +j4k-align detects the forge per repo from its git origin and audits and aligns config across both GitHub (via `gh`) and the self-hosted Forgejo instance (`code.j4k.dev`, via `fgj`) end to end — there is no Forge gap to work around. + +When you hit a config issue in one repo — a misbehaving lint rule, a missing tsconfig field, a workflow that breaks on a new dependency — ask: **"Will this come up again in another repo?"** If yes, the fix belongs in `j4k-align`. Patching one repo lets the same trap resurface elsewhere; encoding it in the audit catches it everywhere. + +## Traits, triggers, and the extension loop + +Repos are classified by **traits** — labels like `pnpm-package`, `react`, `nextjs`, `external-references`, `private-oci-publish` defined in `src/alignment/schemas.ts`. Each trait is assigned by a **trigger** — a signal from the repo (file presence, `package.json` dependency, `.gitmodules` content, registry config). Traits gate checks and template selection. + +Touchpoints when extending the audit: + +- Trigger: `src/traits/signals.ts` +- Trait assignment: `src/traits/build-traits.ts` (enum in `src/alignment/schemas.ts`) +- Trait-gated check: `src/cli/verify/check-*.ts`, wired in `src/cli/verify/run-checks.ts` +- Fix implementation: `src/checks/*.ts`, wired in `src/cli/fix/local-project-configs/fix-local-tool-configs.ts` +- Templates: `templates/`, with `TraitSelection` guards in `src/resolve-template-files.selections.ts` + +**Fix the signal, not the symptom.** If a repo should have a trait but doesn't — or has one it shouldn't — the trigger is wrong. Repair `src/traits/signals.ts` rather than hardcoding the trait downstream. Verify with `j4k-align --repo owner/name`, then `--fix` to confirm remediation. + +## Forbid the trap, not just the current case + +When you hit a known footgun — a setting that silently breaks types, a flag that disables a guarantee, a path pattern that traps on edge cases — add a check that rejects that value across every applicable trait, not one that only repairs the current repo. Forbid the specific bad shape, name the check clearly, and record the failure mode it prevents inside the check so the reasoning travels with the code. + +## Scope + +j4k-align governs cross-repo configuration: build, lint, format, tsconfig, workflows, rulesets (GitHub) / branch protection (Forgejo), repo settings on either forge, automation secrets and variables, and managed files (git hooks, `release.config.mjs`, etc.). Project-specific business code, product schema, and feature behavior stay in their repos. + +## Anti-patterns + +- `eslint-disable`, `// @ts-ignore`, or `.gitignore` entries that silence a warning other repos will hit identically. +- Hand-editing `.github/workflows/*.yml` or `.forgejo/workflows/*.yml` when the source-of-truth template lives in `templates/`. +- Diverging a per-repo `tsconfig.json` to dodge a check instead of fixing the check. +- Hardcoding a trait or skipping a check when the real bug is in the trigger. + +This is a specialized application of _Fix the Foundation First_ — the foundation here is the trait/trigger/check/fix pipeline. + +# Rule: `eslint-config-axkit` Is Deprecated + +Superseded by oxlint with `@j4k/oxlint-config`; it cannot run under the fleet's `typescript@^7` pin, which breaks `typescript-eslint`. Never add it to a project — when touching a repo that still lints through it, migrate to oxlint instead. + +# Rule: Use the j4k Custom Build of `fgj` + +The j4k fork at [`codeberg.org/jercik/fgj`](https://codeberg.org/jercik/fgj) ships features not yet in upstream `romaintb/fgj` — `fgj pr review` with inline comments, `fgj pr review resolve`/`unresolve` for conversation resolution (against the j4k Forgejo fork's resolution API), `fgj pr checks` for a PR's combined CI status, the `fgj pr list --base`/`--head` server-side filters, org-scoped `fgj actions` secrets/variables (with pipeable secret input), and `git_hosts` aliases for remotes whose SSH host differs from the Forgejo API host. Features leave this list as they merge: the generic `fgj api` passthrough and `fgj repo view --json` landed upstream in `v0.5.0` (2026-07). Other rules here lean on these commands, so prefer this build over the stock Homebrew one. + +**Check the build, not the version number.** The `-j4k.N` suffix on `fgj --version` is the durable tell; a bare upstream version (e.g. `0.4.0`) means stock. Gate on the suffix, which survives version bumps: + +```bash +fgj --version | grep -q j4k || echo "stock fgj — install the j4k build" +``` + +The suffix only proves the fork lineage, not any one feature — features accrete across `-j4k.N` releases, so an older fork build passes the gate while missing a newer verb. When a workflow depends on a specific command, gate on the parent's subcommand listing. An exit-code probe (`fgj pr review resolve --help`) false-passes on the build it exists to catch: cobra reads the unknown word as a positional argument and answers `--help` with the parent's help, exit `0`. The listing discriminates — scoped to the `Available Commands:` block, so an indented prose or example line that happens to start with a verb name can't satisfy it — and checks every verb the workflow needs, since the accretion argument above applies verb by verb: + +```bash +help=$(fgj pr review --help) \ + || { echo "fgj pr review --help failed — is fgj installed?" >&2; exit 1; } +verbs=$(awk '/^Available Commands:/{f=1;next} /^[^[:space:]]/{f=0} f' <<<"$help") +for v in resolve unresolve; do + grep -qE "^[[:space:]]+$v([[:space:]]|$)" <<<"$verbs" \ + || { echo "fgj build has no $v verb — install the newest v*-j4k.* release" >&2; exit 1; } +done +``` + +The `--help` failure branch is the missing-or-broken-binary case — cobra answers `--help` with exit `0` even on a build with no `pr review` at all (the parent-fallback above), so a nonzero exit means the command never ran. Every other shape falls through to the per-verb check, whose message names the right remedy for all of them: the parent `pr` help of a stock or pre-`pr review` fork build, and even a `pr review` leaf command whose help prints no `Available Commands:` block at all — released j4k builds always print it (`pr review` shipped with `list` and `comments` already registered), but the probe no longer leans on that history to route the failure. The `exit 1` makes the snippet a hard gate for scripts; a workflow that can degrade instead — like the feedback-processing skill, which skips only the resolve step on a failed probe — runs the same probe and branches on its exit status rather than dying. + +Install the newest `v*-j4k.*` tag from the [releases page](https://codeberg.org/jercik/fgj/releases) — `scripts/install.sh` is checksum-verified for CI and local machines; in a Dockerfile, pull the `linux_{amd64,arm64}.tar.gz` release asset directly. Binaries are static and CGO-free, so they run anywhere (alpine, distroless, scratch). + +For Forge repos whose `origin` uses a transport-only host such as `code.tail.j4k.dev`, configure the API host with a Git alias instead of passing the SSH host as `--hostname`: + +```bash +fgj auth login --hostname code.j4k.dev --git-host code.tail.j4k.dev +``` + +Features are upstreamed one small PR at a time. Until a feature lands upstream and ships in a stock release, the j4k build is the source of truth. + +# Rule: Fix the Foundation First + +When you hit an issue likely to recur, stop and solve the underlying problem rather than working around it. The upfront investment pays for itself every time the issue would have resurfaced. + +For example: if you don't know how an API works and will need it repeatedly, don't guess at endpoints — build a tool that fetches and displays its docs. If a manual step keeps recurring, automate it. If knowledge is missing, capture it in a script, command, or doc so it's available next time. + +The key question: _"Will this come up again?"_ If yes, fix the root cause now. + +# Rule: Detect the Forge — GitHub (`gh`) vs Forgejo (`fgj`) + +Repos are mid-migration across two forges, so **never assume `gh`** — it only speaks GitHub's API. Detect the provider from `origin` before any forge operation (PRs, issues, CI/checks, releases, repo metadata, branch protection) and use the matching CLI. + +- **GitHub** — `github.com`, CLI `gh`, names untouched (`Jercik/j4k-cluster`). +- **The Forge** — self-hosted Forgejo at `code.j4k.dev`, CLI [`fgj`](https://codeberg.org/romaintb/fgj). Repos live under `j4k/` and drop the `j4k-` affix (`j4k-cluster` → `j4k/cluster`; an already-unprefixed repo like `setup-atlas` keeps its name). + +## Detection + +Run `npx -y repoq@latest forge --json`. The explicit tag prevents `npx` from reusing a stale cached release. It normalizes every URL shape (scp-like, `ssh://`, `https://`) and handles the `j4k-` affix that a hand-rolled `sed` gets wrong, returning `provider` (`github`/`forgejo`/`unknown`), `cli`, `slug`, `hostnameFlag`, `apiHost`, `apiBase`, plus `host`/`webHost`/`sshHost`/`owner`/`repo`. Reach for `apiHost` when passing a host to `fgj` — `hostnameFlag` is a two-word string that zsh, which does not word-split expansions, hands `fgj` as a single argument. Treat `unknown` as a hard stop. + +## Driving `fgj` + +`fgj` mirrors `gh`'s verbs (`pr`, `issue`, `release`, `repo`, `label`, `milestone`, with `--json` on the read verbs). For a PR's combined CI status, use `fgj pr checks ` (add `--json` for machine-readable output; exit codes match `gh pr checks` — `0` success/warning/skipped/no-checks, `1` failure/error, `8` pending). CI run history lives under `fgj actions run list|view|watch`; for step logs use `fgj actions run view --log` (also `--log-failed`, `-j `) — don't decompress the on-disk `actions_log/*.zst` chunks. + +When running inside the target repo, the j4k build of `fgj` can auto-detect the repo and API host. If `origin` uses a transport-only SSH host such as `ssh://git@code.tail.j4k.dev:2222/…`, configure that hostname as a `git_hosts` alias under the real API host (`code.j4k.dev`) and do not pass the SSH host as `--hostname`. + +For scripts, cross-repo commands, or work launched outside the target repo, use `repoq forge --json` and pass the returned host and slug explicitly: `fgj --hostname "$apiHost" -R "$slug" ...`. Explicit flags are still the most deterministic shape when cwd or remote config might not describe the target repo. The leading position is convention, not requirement — `--hostname` is a root persistent flag that cobra parses before or after the subcommand, so a trailing literal `--hostname ` (the `fgj auth` examples) is equally correct; the hazard the convention guards is the two-word `$hostnameFlag` expansion, which breaks at any position. One nuance, not an exception: the `auth` verbs declare a local `--hostname` that shadows the root flag, but cobra hands a leading flag to that local flag too, so both positions keep working — the difference is only that the shadowed root value never reaches the config fallback, and `auth login` has no fallback at all: pass its `--hostname` explicitly (either position) or it prompts interactively. + +## Beyond `fgj`'s verbs: `fgj api` + +Some operations have no dedicated verb. Reach for the generic `fgj api ` passthrough — it reuses the configured auth and resolved API host. The passthrough started in [the j4k custom build of `fgj`](./fgj-custom-build.md), which the other `fgj` rules already assume, and landed upstream in `v0.5.0` (2026-07); on an older stock `fgj` without it, fall back to `curl "$apiBase/"` with a token from `fgj auth token --hostname "$apiHost"` — capture it first and require it non-empty (`token=$(fgj auth token --hostname "$apiHost")`; an empty substitution would send an unauthenticated request whose `401` reads as an instance fault instead of a missing token), then pass it as a `curl` config on stdin — `printf 'header = "Authorization: token %s"\n' "$token" | curl -fsS --config - "$apiBase/"` — not as an `-H` argv header any process on the machine could read from the process table. + +- **Branch protection** (the Forge's replacement for GitHub "rulesets"): `/repos/{owner}/{repo}/branch_protections[/{name}]`. +- **A PR's combined CI status**: use the dedicated `fgj pr checks ` command instead — not the raw passthrough (add `--json` for machine-readable output; exit codes match `gh pr checks` — `0` success/warning/skipped/no-checks, `1` failure/error, `8` pending). +- **Combined commit status for an arbitrary commit not tied to a PR**: `/repos/{owner}/{repo}/commits/{sha}/status`. +- **Repo metadata as JSON**: `/repos/{owner}/{repo}` — or just `fgj repo view --json` (upstream since `v0.5.0`, alongside the passthrough). + +## Inspecting all Forgejo PR feedback + +Forgejo splits PR discussion across three API surfaces: issue comments at `/repos/{owner}/{repo}/issues/{number}/comments`, review summaries at `/repos/{owner}/{repo}/pulls/{number}/reviews`, and inline comments under each review at `/repos/{owner}/{repo}/pulls/{number}/reviews/{review-id}/comments`. Reading one surface misses the others: a review's summary body often carries only a count ("Found 2 medium issues") while the findings live in its inline comments, and responses land as issue comments. Web URLs anchor inline comments as `#issuecomment-`, but the API exposes them only under `/pulls/{number}/reviews/{review-id}/comments`, never in `/issues/{number}/comments`. + +Before handing off a PR, prove the review cycle is complete for the current head: + +1. Wait for the `PR Review` action run whose `commit_sha` equals `git rev-parse HEAD` (the run appears asynchronously — retry an empty result), then wait until every job in that run is terminal. A run-level failure or a terminal `fgj pr checks` does not mean all reviewer jobs finished. +2. Read the expected reviewer set from that run's jobs endpoint, not from workflow files in the checkout — `pull_request_target` executes the base branch's workflow. +3. Require every expected non-skipped reviewer job to have a submitted review whose `commit_id` is that head, and `fgj pr checks ` to report the same head in `.sha`. A reviewer job that exits without publishing a verdict blocks handoff — retry it. +4. Sweep all three surfaces (pagination rules below) and repeat until two consecutive sweeps return identical totals and records — a pending review can become visible without changing `X-Total-Count`. +5. After any follow-up push, redo the whole check against the new head; earlier reviews say nothing about the updated commit. + +Pagination on Forgejo 16.0.0: + +- **Reviews**: fetch `?limit=&page=N` for every page through `ceil(X-Total-Count / limit)`, taking the limit from `/settings/api` (`max_response_items`, 50 on code.j4k.dev). Don't stop at an underfilled page — Forgejo filters other users' pending reviews after database pagination, so a short page can precede later submitted reviews. The endpoint sends neither `Link` nor `X-HasMore`, so `fgj api --paginate` cannot prove completeness. Keep `PENDING` reviews in the sweep snapshot but don't treat their unpublished bodies or comments as feedback. A persistent gap between `X-Total-Count` and the deduplicated visible reviews is another user's unpublished draft — record it as a diagnostic, not a handoff blocker. +- **Issue comments**: one fetch returns everything — the endpoint ignores `page` and `limit`. Require the distinct returned IDs to match its `X-Total-Count`. + +`gh-feedback summary --json` aggregates issue comments, the inline review comments it reaches, and their reactions and responses — useful, but not proof of completeness: it omits review summary bodies and its Forgejo pager stops on an underfilled review page. Use it alongside the sweep, not instead of it. + +When processing or handing off a PR — not during a read-only audit — address every actionable finding and acknowledge the response on the PR before reporting completion. + +**Resolving conversations depends on the instance.** The j4k Forgejo fork adds a REST conversation-resolve API; stock Forgejo — upstream and codeberg.org, through 16.0.x — has none. Detect the capability from the `version` endpoint (`fgj --hostname "$apiHost" api version` — explicit host, or a probe run outside the target repo answers for `fgj`'s default host and reads that instance's capability instead; or `curl -fsS "$apiBase/version"` — `-f` so an HTTP error exits nonzero instead of handing the substring test an error body): the version string contains `-j4k` (live: `16.0.1-j4k.1+gitea-1.22.0`) on the fork, and `code.j4k.dev` qualifies — test for the substring, since build metadata (`+gitea-…`) trails the marker. The substring proves fork lineage, not the endpoint: an older `-j4k` server that predates the resolution route passes the test and `404`s on the first resolve — treat that like an unreadable version, skipping resolution for the pass and reporting it. A version without `-j4k` is stock Forgejo (e.g. codeberg.org's `16.0.0-dev-626-32363b81+gitea-1.22.0`); an unreadable version — `404`, network error, a body that isn't JSON with a `.version` string — blocks only resolution, not the feedback pass: process feedback normally, skip the resolve step, and report the failed probe rather than treating it as a stock verdict. This is the server's version — don't infer it from `fgj --version`, which reports the CLI build, a separate j4k fork that talks to stock servers just fine; that gate is effectively always true on this machine while the target instance may well be stock. Comment-minimize exists on neither build — Forgejo has no minimize concept at all. + +On a `-j4k` instance, `gh-feedback` v3.3.0+ owns native transitions for items it tracks; older builds use the feedback skill's qualified direct fallback. Raw-only workflows resolve with `fgj --hostname "$apiHost" pr review resolve -R "$slug"` and reopen with `… unresolve` (j4k `fgj` build `v0.5.0-j4k.4`+) — the explicit `-R` matters as much here as anywhere, since an omitted repo falls back to cwd detection. Any comment id in the thread works: the command lists the PR's reviews and their comments itself, walks to the thread's anchor, names the anchor it targeted, and reports the updated `resolver` (`--json` returns the updated anchor comment). Gate on the parent's subcommand listing per the j4k `fgj` build rule — on an older j4k build without the verb the call dies with cobra's `accepts 1 arg(s), received 3`, a usage error that means the verb is missing, not that you mistyped, and the fix is installing the newest build — not scripting a raw fallback; the REST path behind the verb goes deliberately unnamed here so it can't accrete one. Argument mistakes fail before any write with their own messages — `failed to list reviews` for a wrong PR number or slug, `not an inline review comment` for a review summary, issue comment, or foreign id (they aren't conversations) — and that second message also covers an inline id deleted since your sweep listed it, because the command re-lists every review comment before writing: re-run the sweep before reading it as your own mistyped id. A `404` from the resolve call itself means the server has no resolution API — stock Forgejo, or a `-j4k` server predating the route; the only deleted-id `404` is a comment vanishing in the instant between the command's own listing and its write. Token auth, same gate as other PR writes; success returns the updated anchor, whose `resolver` reflects real DB state and stays the original resolver on an idempotent re-resolve. + +**Verification still needs the conversation partition.** The resolve endpoint writes `resolver` to exactly the comment it is handed, and the UI reads a conversation's resolved state only from its anchor — that is why the command walks to the anchor before writing, and why the completeness sweep must read each anchor's `resolver` rather than any reply's. The API exposes no threading field, so derive conversations from the sweep's inline comments: a conversation is the code comments sharing a `path`, a side with its display line, and a `pull_request_review_id` — replies join the review they answer, while comments that different reviews leave at the same line are separate conversations, each resolved on its own. The side is whichever of `position` (new side) / `original_position` (old side) is nonzero — the unused side reports `0`, and old-side and new-side comments at the same number are distinct conversations; both at `0` is the stored line-`0` edge (the server keeps one signed line and reports it on a single side), which groups like any other value — same review, same path, line `0`, one conversation — and the display line is that number plus `extra_lines_count` — upstream Forgejo API since `v16.0.0`, where multi-line comments landed, not a fork field: Forgejo buckets a multi-line comment at the _end_ of its range, so a comment spanning 55–60 and a single-line comment at 60 from the same review are one conversation, and grouping by the raw `position` pair alone splits them and mis-picks the anchor. The anchor is the conversation's earliest comment (`created_at`, ties by lowest `id`). This partition is the one the API listings and the Conversations tab render; the sweep's `position` is the line as of the comment's own commit, while the diff view re-blames comments to the current head and can merge same-review buckets after lines move — don't expect it to mirror the derivation. `gh-feedback` derives this same native partition when reading or changing resolver state, but its feedback items still thread by reply markers: an item id is therefore not necessarily the anchor, and two findings one review left at the same display line are separate items sharing one conversation whose single resolve state — the anchor's, per the write-where-handed endpoint above, so a sibling root comment can carry a stray `resolver` from a direct write without changing what renders — speaks for both. Confirm the anchor's `resolver` is set after resolving — the per-review comment listings the sweep already fetches return it on every comment — and read it first to know the current state before unresolving. + +On a stock instance no resolve endpoint exists — the review `dismissals` endpoint dismisses a review's verdict, not a conversation, and the web "Resolve conversation" route authenticates by browser session cookie only, so a token POST 303-redirects to `/user/login` and resolves nothing. Track "done" by reaction there instead of pretending to resolve. + +# Rule: Generated Agent-File Drift Is Expected + +Automated tooling rewrites tracked agent instruction files (`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md`) whenever shared rules change, so these files may sit modified for long stretches. + +When they are the only modifications, treat the repository as clean — don't stash, revert, or delete the changes, and don't commit them on their own. Include them in the next substantive commit or pull request instead. + +# Rule: Local Third-Party Checkouts + +Before searching the web or relying on memory for an external library, check `~/Developer/third-party/` — it holds dozens of third-party repos cloned locally for reference. The source is faster and more authoritative than any secondary description. + +Run `ls ~/Developer/third-party/` to see what's available, then explore as with any local code. Don't clone new repos into this directory unsolicited — the user curates it. + +# Rule: Target Bash 3.2 + +Unless the user specifies otherwise, write shell scripts for bash 3.2 — the default `/bin/bash` on macOS. + +When using `printf`, do not pass a literal string starting with `-` as the format string. Use `printf '%s\n' "$value"` or `echo` so Bash 3.2 does not parse the marker as an option. + +Backslash line continuation inside a `for … in` word list across multiple lines is fragile in Bash 3.2 and can fail with "syntax error near unexpected token `done`". Collect the patterns in an array (`globs=(g1 g2 g3)`) and iterate with a nested loop, or put all patterns on a single line. + +# Rule: No Backwards Compatibility + +Prioritize the best shape of the current codebase over backwards compatibility. When something changes, change it completely — rename the function, delete the old flag, stop reading the old config path. Let the code reflect what it _is_, not what it _was_; version control remembers what was there. + +Remove every form of backwards-compatibility ballast: + +- Aliases and re-exports keeping old import paths or type names working +- Fallback reads from old config locations or env var names +- Renamed-but-kept flags, options, or CLI arguments +- Feature flags gating already-shipped changes +- Underscore-prefixed "unused" variables kept to silence lint +- `// removed: …` or `// TODO: delete in v3` comments marking absent code +- Deprecated types re-exported from their old module +- Database migrations committed before the project has real users — reset the dev DB and iterate + +Compatibility code accumulates as dead weight: it obscures intent, inflates surface area, and forces every future reader to reason about states that no longer exist. + +## Database migrations + +Committed migrations are permanent — every schema tweak becomes a file that travels with the project forever, even if the only "old" schema lived for an afternoon in a dev DB. Before a deployment holds data you can't discard, reset the dev DB and iterate on the schema directly; commit only the current state. A squashed clean initial schema is far cheaper to reason about than a chain of pre-release churn. + +Once real users have real data, the mode flips: every schema change becomes a migration, no exceptions. The cutover is "first deployment with persistent real data," not "first commit" or "first merged PR." The same reasoning extends to any on-disk or over-the-wire format you cannot discard — serialized caches, stored session blobs, published API shapes — iterate freely before the first real reader exists, then lock it down. + +# Rule: Access OrbStack Machines with `orb`, Not SSH + +To run a command inside an OrbStack Linux machine, use `orb -m -u ` (wrap in `bash -lc '…'` when you need a login shell, PATH, or pipes). The standing dev VM is `debian` with user `j4k`: + +```bash +orb -m debian -u j4k bash -lc 'node --version' +``` + +Don't reach for `ssh @.orb.local` — it authenticates by public key and fails with `Permission denied (publickey)` unless that user already has your key in `authorized_keys`. `orb` reuses OrbStack's host identity mapping, so it needs no key and works for any existing user on the machine. + +# Rule: Package Manager Execution + +How different package manager commands resolve binaries: + +| Command | Behavior | +| ----------------- | ----------------------------------------------------------------------- | +| `pnpm exec foo` | Runs from `./node_modules/.bin`; falls back to system PATH | +| `pnpx foo` | Always fetches from registry (uses dlx cache); ignores local installs | +| `npx foo` | Checks local `node_modules/.bin` → global → downloads from registry | +| `npx foo@version` | Resolves version, uses local if exact match exists, otherwise downloads | + +`pnpx` is an alias for `pnpm dlx`. + +# Rule: Prefer OrbStack Locally + +Use OrbStack as the local container and Linux VM runtime on macOS — not Docker Desktop, Colima, or a Podman machine. The `docker` and `docker compose` CLIs work unchanged; `orbctl` (aliased `orb`) creates and manages full Linux VMs. + +OrbStack has faster cold starts, lower idle CPU and memory, native macOS file sharing without bind-mount workarounds, and a single tool for both containers and VMs. Assume any container or VM workflow on this host runs through OrbStack. + +For ad hoc Linux VM testing, see the `orbstack-ad-hoc-vm` skill. + +# Rule: Prefer TypeScript Over Python + +When writing new code and the user states no language requirement, default to TypeScript. This yields to explicit user input: write Python when the user asks, when the task lives in a Python codebase, or when the ecosystem forces it (data science, ML, a Python-only library). + +# Rule: Project Skill Symlinks + +Keep the real skill in `.agents/skills//` and treat `.claude/skills/` as a symlink to it, not a second copy. Create the Claude entry with `ln -s ../../.agents/skills/ .claude/skills/` so both locations point at the same source of truth. + +# Rule: Runtime Tool Discovery + +When a workflow references a custom CLI — a local script or anything you may not already know — run ` --help` before first use. The help output is the authoritative source for subcommands, flags, and usage; rules only name the tool and rely on `--help` to teach you the rest at runtime. + +Compose these tools with pipes like any Unix CLI, and prefer machine-readable formats (`--porcelain`, `--json`) over parsing human-readable output. + +# Rule: Repository Scripts + +`scripts/` is the default home for the repository's operational tooling — automation, helpers, and one-offs that maintain the repo but aren't part of what it ships. + +**Look there first.** If `scripts/` exists, `ls scripts/` and `jq '.scripts' package.json` before writing anything new — extend what's there rather than forking it. + +**Put new scripts there by default.** Anything worth committing — release helpers, submodule updates, git hooks, recurring chores — goes in `scripts/`, wired through `package.json` so callers invoke it by name rather than remembering the path. + +# Rule: Set an Explicit Timeout for Long CI Waits + +Waiting on CI to finish — `gh run watch`, `fgj actions run watch`, or a poll loop over `gh run list --commit ` — routinely outlasts an agent shell tool's default command timeout (Claude Code's Bash tool defaults to 120s, raisable to 600s). Pass an explicit longer timeout (e.g. 420000 ms) to the command invocation, or the wait is killed mid-run and reports a false failure. + +# Rule: Sub-Agent Delegation + +Spawn sub-agents liberally. A sub-agent encapsulates a chunk of work behind a simple interface: briefed on the task, it gathers its own context, works autonomously, and hands back only the result — trust the process and engage with the outcome. The main agent's job is to frame tasks, dispatch, and synthesize results. + +**Delegate, move on, verify.** A task of many simple steps is prime delegation material — deploying an Ansible playbook and combing its verbose logs where nearly every task just reports ok, provisioning a throwaway OrbStack VM with Node, Docker, and Postgres installed, clicking through a multi-page web flow filling fields and pressing buttons, watching a CI run, applying a bulk mechanical edit. Hand it to a background sub-agent, move on to other work while it runs, and when it finishes, spawn a fresh sub-agent to confirm the work was done correctly — and for critical work, several adversarial reviewers, each attacking the result through a different lens. + +**Skills delegate too.** Instead of invoking a skill yourself, consider handing it to a sub-agent: name the skill and the inputs, and the sub-agent loads it, follows the workflow, and returns the result — the skill's full instruction set never enters the main context. `agent-browser` is the perfect shape for this: "log in to the site and check that such-and-such feature works" is a one-line instruction with a one-line answer, and everything in between — dozens of tool calls, failed selectors, retries — stays encapsulated in the sub-agent. Whether that fits is a per-skill call. + +# Rule: TSV Parsing + +`awk` splits on any whitespace by default, silently breaking on TSV values containing spaces. For tab-separated output (often `--porcelain` flags), set the delimiter explicitly: + +```bash +# BAD: prints "name" instead of "name with spaces" +printf 'id\tname with spaces\tstatus\n' | awk '{ print $2 }' + +# GOOD — pick one: +awk -F'\t' '{ print $2 }' +cut -f2 # cut defaults to tab +while IFS=$'\t' read -r a b c; do …; done +``` + +Empty fields are a second trap for the `read` form only: tab is IFS _whitespace_, so runs of tabs collapse into one delimiter and an empty middle field shifts every value after it — `awk -F'\t'` and `cut -f` are immune. When a field can be empty, prefer `awk`/`cut`, keep nullable fields last, or in zsh double the tab (`IFS=$'\t\t'`), which the manual defines as demoting it to a hard delimiter that preserves empty fields; bash has no doubled form — it silently ignores the doubling and shifts the fields anyway, so a bash-run copy of the zsh idiom reinstates the exact bug it exists to prevent, with no diagnostic. + +Not every `--porcelain` is TSV — `git worktree list --porcelain` is space-separated per line. Sample output before picking a delimiter. + +# Rule: Reach for Unix-Native Primitives Before Inventing Abstractions + +Use the OS as the first control plane. Before proposing a registry, supervisor, scheduler, logger, IPC layer, config store, or discovery protocol, check whether argv, environment variables, inherited file descriptors, filesystem paths, Unix-domain sockets, ports, signals, stdout/stderr, cron, systemd, or XDG paths already solve it. + +Two primitives cover almost everything: + +- **Handoff at fork/exec** — parent passes addresses to children via args, env vars, or inherited FDs (`SSH_AUTH_SOCK`, systemd socket activation). +- **Well-known names in a shared namespace** — filesystem paths and TCP/UDP ports (`/var/run/docker.sock`, port 22). The filesystem is the service directory. + +Common problems map directly: process discovery → socket at a conventional path or env var; IPC → Unix-domain socket, named pipe, or signal; supervision → systemd or another init; scheduling → cron or systemd timer; logging → stdout/stderr; config → XDG config dir. + +For Node apps needing a config, data, cache, log, or temp directory, default to [`env-paths`](https://github.com/sindresorhus/env-paths). It returns the right location per platform — XDG on Linux, `~/Library/...` on macOS, `%APPDATA%` on Windows — so you don't hand-roll `process.platform` branches that drift. Pass a namespace (`envPaths('my-app')`) and use the returned `config`, `data`, `cache`, `log`, `temp` paths directly. + +**Don't build a second control plane.** Reject the native primitive only when you can name the concrete property it cannot provide: distributed discovery across hosts, authorization the OS namespace cannot enforce, schema evolution for a long-lived wire format, multiplexing many streams over one transport, binary streaming with backpressure, or cross-platform targets where no equivalent primitive exists everywhere. + +Otherwise the native primitive wins. Designs that don't compose with pipes, signals, and conventional file locations pay a tax forever. + +# Rule: Use Native TypeScript Execution + +Use Node 24+ and run `.ts` files directly with `node script.ts`. Node strips types at runtime — no `tsx`, no `ts-node`, no `tsc` build step. + +Default to `.ts` over `.mjs` for new scripts and to `node` over `tsx` in `package.json`. + +# Rule: Sub-Agent Model Selection + +Never launch a sub-agent on any model other than `gpt-5.6-terra` and `gpt-5.6-sol` — use exactly two configurations, routed by the deliverable: + +- `gpt-5.6-terra` at `high` — the executor, when the deliverable is facts or a completed task: running commands and test suites, searching a codebase for symbols or call sites, browsing and scraping pages, collecting evidence against given criteria, extracting data from files or docs, applying a specified change. Multi-step, tool-heavy, trial-and-error work belongs here — failed calls and adjusted arguments are churn the executor absorbs, handing back only the outcome. +- `gpt-5.6-sol` at `xhigh` — when the deliverable is a conclusion: reviewing work, synthesizing findings into a report, diagnosing a root cause, choosing an approach, weighing tradeoffs, designing a plan. + +"Find every `fetchUser` call site and list file:line" is executor work — the deliverable is locations the driver interprets. "Review this diff for bugs" is `gpt-5.6-sol` work — the deliverable is the judgment itself. + +Set `model` and `reasoning_effort` explicitly on every sub-agent. Never raise the executor above `high` — a task that seems to need more is delivering conclusions, so it belongs on `gpt-5.6-sol` — and never lower `gpt-5.6-sol` below `xhigh`. diff --git a/archive/global-targets/copilot-copilot-instructions.md b/archive/global-targets/copilot-copilot-instructions.md new file mode 100644 index 0000000..f326719 --- /dev/null +++ b/archive/global-targets/copilot-copilot-instructions.md @@ -0,0 +1,427 @@ +# Rule: 1Password Commit Signing + +This machine signs git commits via 1Password. Any signing error during `git commit` — 1Password socket errors, "failed to sign the data", "fatal: failed to write commit object" — usually means 1Password is locked. Ask the user to unlock 1Password, then retry the commit. + +# Rule: `AGENTS.md` Is Generated — Edit the Source + +`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md` and the like are generated by the `sync-rules` CLI. The next `sync-rules` run overwrites direct edits. + +To change what an agent reads, edit one of two sources: + +- **Rule content** in `~/Developer/j4k/setup-atlas/rules//*.md` — reword an existing file or add a new one under the appropriate category. +- **Which rules apply to which repo** in `/Users/jercik/Library/Preferences/sync-rules/config.json` — add or remove a glob (e.g., `"nodejs/*.md"`) under a `projects[].path` entry, or under `global` / `globalOverrides` for cross-repo defaults. + +Run `sync-rules` after either edit. + +# Rule: Automatic Repository Alignment Scope + +When the user asks to align or audit "all my repositories", that means the repository checkouts directly under `~/Developer/jercik/`, `~/Developer/j4k/`, and `~/Developer/j4k-oss/` — nothing outside those three directories. + +# Rule: Canonical Repository Checkout Layout + +Place every owned repository under `~/Developer//`, using the remote owner and repository slugs rather than a locally invented prefix: + +- GitHub `Jercik/example` → `~/Developer/jercik/example` +- GitHub `validationcloud/example` → `~/Developer/validationcloud/example` +- Forgejo `j4k/example` → `~/Developer/j4k/example` +- Forgejo `j4k-oss/example` → `~/Developer/j4k-oss/example` + +Use the remote repository name verbatim. A historical local prefix is not part of the name: Forgejo `j4k/align` belongs at `~/Developer/j4k/align`, never `~/Developer/j4k-align`. + +Git worktrees live in the same owner directory as the main checkout, with the branch name appended to the directory name as `-`: a worktree for branch `foo` of `~/Developer/j4k/align` goes at `~/Developer/j4k/align-foo`. Create worktrees with `worktree-add ` from inside the repository — it places the new checkout at that canonical path automatically, copies useful local files, and installs dependencies. + +Before cloning, resolve the forge and owner/repository slug, create the owner directory, and pass the canonical destination explicitly. The account-aware `gh` shim selects the ValidationCloud account and SSH key for `validationcloud/*` and `lukasz-jercinski-vc/*` targets, and the personal account otherwise. Forgejo clones use the canonical tailnet SSH transport and an explicit destination. `code.tail.j4k.dev` is reachable only while the machine is connected to the Tailscale network — a clone or fetch that hangs or can't connect usually means the tailnet is down, not that the key or remote is wrong; check the Tailscale connection before debugging SSH. + +Third-party source remains under `~/Developer/third-party/` and is never reorganized by owner. Do not infer an owner for a Git root with no remote or for a plain local directory; leave it in place until the user classifies it. + +# Rule: You Share This Workspace + +Other agents and the user may have uncommitted WIP in the working tree, and new changes can appear mid-session. Don't assume unexpected state came from your edits, and don't stash, overwrite, or commit work you didn't make — even stash-then-pop can confuse another agent whose tree state shifts underfoot. + +If you need an isolated tree, ask the user about creating a git worktree and move your changes there. + +# Rule: Conventional Commits + +Write every git commit message and pull request title in Conventional Commits format (`type: subject`). + +Before authoring a PR title and body, load the `pr-writing-style` skill if it is installed — it owns the prose: title wording, body shape, what gets cut. + +# Rule: Create PRs on the Repo's Forge (No Shell Expansion) + +Detect the forge first — [the forge detection rule](./forge-provider-detection.md) owns the how — and use the matching PR tool: `gh` for GitHub, `fgj` for Forgejo. Don't reach for `gh` reflexively; it can't open a PR on Forgejo. + +The shared trap is shell expansion of multi-line Markdown bodies. A double-quoted body string lets the shell expand backticks and `$...` before the CLI sees it, mangling code blocks and variable references. Pass the body so the shell never scans it. + +**GitHub (`gh`)** — `--body-file` with a single-quoted heredoc (`'EOF'` disables all expansion): + +```bash +gh pr create --title "docs: clarify example" --body-file - <<'EOF' +Clarifies that the `..${sep}` check doesn't block `..foo/bar.txt`. +EOF +``` + +**Forge (`fgj`)** — `fgj pr create` takes only `-b` (no `--body-file`), so write the body with a single-quoted heredoc, then pass it by command substitution. The file's bytes become the argument verbatim; the shell does not re-scan them for expansion: + +```bash +forge=$(mktemp) +npx -y repoq@latest forge --json > "$forge" +apiHost=$(jq -r '.apiHost' "$forge") # e.g. code.j4k.dev, codeberg.org +slug=$(jq -r '.slug' "$forge") +body=$(mktemp) +cat > "$body" <<'EOF' +Clarifies that the `..${sep}` check doesn't block `..foo/bar.txt`. +EOF +fgj --hostname "$apiHost" pr create -R "$slug" \ + --title "docs: clarify example" --base main --head my-branch \ + -b "$(cat "$body")" +rm -f "$body" "$forge" +``` + +When running from the target repo with the j4k build configured, `fgj` can auto-detect the repo and API host, including `git_hosts` aliases for SSH-only transport hosts. For scripts, cross-repo work, or commands launched outside the target repo, still pass `--hostname "$apiHost"` and `-R "$slug"` from `repoq forge --json`; explicit flags remove cwd and remote-alias ambiguity. `fgj pr edit -F ` accepts a body file if you'd rather create then edit. + +Verify with the matching CLI: `gh pr view --json title,body,url` or `fgj --hostname "$apiHost" pr view -R "$slug" --json`. + +# Rule: Doc-Comments and Example Snippets Are Documentation + +Audit JSDoc/docstrings and example code in source with the same rigor as markdown. Example calls must type-check against the current signature; a required `T | undefined` field omitted from an example is drift even though it "looks" fine. + +# Rule: Encode Cross-Repo Config Learnings in j4k-align + +`j4k-align` (in `~/Developer/j4k/align/`) audits and aligns repo settings on either forge, automation inputs, workflows, rulesets (GitHub) / branch protection (Forgejo), and managed files across every repo. Its checks and templates are the source of truth for cross-repo configuration — on-disk state drifts; the audit defines what "correct" means. + +j4k-align detects the forge per repo from its git origin and audits and aligns config across both GitHub (via `gh`) and the self-hosted Forgejo instance (`code.j4k.dev`, via `fgj`) end to end — there is no Forge gap to work around. + +When you hit a config issue in one repo — a misbehaving lint rule, a missing tsconfig field, a workflow that breaks on a new dependency — ask: **"Will this come up again in another repo?"** If yes, the fix belongs in `j4k-align`. Patching one repo lets the same trap resurface elsewhere; encoding it in the audit catches it everywhere. + +## Traits, triggers, and the extension loop + +Repos are classified by **traits** — labels like `pnpm-package`, `react`, `nextjs`, `external-references`, `private-oci-publish` defined in `src/alignment/schemas.ts`. Each trait is assigned by a **trigger** — a signal from the repo (file presence, `package.json` dependency, `.gitmodules` content, registry config). Traits gate checks and template selection. + +Touchpoints when extending the audit: + +- Trigger: `src/traits/signals.ts` +- Trait assignment: `src/traits/build-traits.ts` (enum in `src/alignment/schemas.ts`) +- Trait-gated check: `src/cli/verify/check-*.ts`, wired in `src/cli/verify/run-checks.ts` +- Fix implementation: `src/checks/*.ts`, wired in `src/cli/fix/local-project-configs/fix-local-tool-configs.ts` +- Templates: `templates/`, with `TraitSelection` guards in `src/resolve-template-files.selections.ts` + +**Fix the signal, not the symptom.** If a repo should have a trait but doesn't — or has one it shouldn't — the trigger is wrong. Repair `src/traits/signals.ts` rather than hardcoding the trait downstream. Verify with `j4k-align --repo owner/name`, then `--fix` to confirm remediation. + +## Forbid the trap, not just the current case + +When you hit a known footgun — a setting that silently breaks types, a flag that disables a guarantee, a path pattern that traps on edge cases — add a check that rejects that value across every applicable trait, not one that only repairs the current repo. Forbid the specific bad shape, name the check clearly, and record the failure mode it prevents inside the check so the reasoning travels with the code. + +## Scope + +j4k-align governs cross-repo configuration: build, lint, format, tsconfig, workflows, rulesets (GitHub) / branch protection (Forgejo), repo settings on either forge, automation secrets and variables, and managed files (git hooks, `release.config.mjs`, etc.). Project-specific business code, product schema, and feature behavior stay in their repos. + +## Anti-patterns + +- `eslint-disable`, `// @ts-ignore`, or `.gitignore` entries that silence a warning other repos will hit identically. +- Hand-editing `.github/workflows/*.yml` or `.forgejo/workflows/*.yml` when the source-of-truth template lives in `templates/`. +- Diverging a per-repo `tsconfig.json` to dodge a check instead of fixing the check. +- Hardcoding a trait or skipping a check when the real bug is in the trigger. + +This is a specialized application of _Fix the Foundation First_ — the foundation here is the trait/trigger/check/fix pipeline. + +# Rule: `eslint-config-axkit` Is Deprecated + +Superseded by oxlint with `@j4k/oxlint-config`; it cannot run under the fleet's `typescript@^7` pin, which breaks `typescript-eslint`. Never add it to a project — when touching a repo that still lints through it, migrate to oxlint instead. + +# Rule: Use the j4k Custom Build of `fgj` + +The j4k fork at [`codeberg.org/jercik/fgj`](https://codeberg.org/jercik/fgj) ships features not yet in upstream `romaintb/fgj` — `fgj pr review` with inline comments, `fgj pr review resolve`/`unresolve` for conversation resolution (against the j4k Forgejo fork's resolution API), `fgj pr checks` for a PR's combined CI status, the `fgj pr list --base`/`--head` server-side filters, org-scoped `fgj actions` secrets/variables (with pipeable secret input), and `git_hosts` aliases for remotes whose SSH host differs from the Forgejo API host. Features leave this list as they merge: the generic `fgj api` passthrough and `fgj repo view --json` landed upstream in `v0.5.0` (2026-07). Other rules here lean on these commands, so prefer this build over the stock Homebrew one. + +**Check the build, not the version number.** The `-j4k.N` suffix on `fgj --version` is the durable tell; a bare upstream version (e.g. `0.4.0`) means stock. Gate on the suffix, which survives version bumps: + +```bash +fgj --version | grep -q j4k || echo "stock fgj — install the j4k build" +``` + +The suffix only proves the fork lineage, not any one feature — features accrete across `-j4k.N` releases, so an older fork build passes the gate while missing a newer verb. When a workflow depends on a specific command, gate on the parent's subcommand listing. An exit-code probe (`fgj pr review resolve --help`) false-passes on the build it exists to catch: cobra reads the unknown word as a positional argument and answers `--help` with the parent's help, exit `0`. The listing discriminates — scoped to the `Available Commands:` block, so an indented prose or example line that happens to start with a verb name can't satisfy it — and checks every verb the workflow needs, since the accretion argument above applies verb by verb: + +```bash +help=$(fgj pr review --help) \ + || { echo "fgj pr review --help failed — is fgj installed?" >&2; exit 1; } +verbs=$(awk '/^Available Commands:/{f=1;next} /^[^[:space:]]/{f=0} f' <<<"$help") +for v in resolve unresolve; do + grep -qE "^[[:space:]]+$v([[:space:]]|$)" <<<"$verbs" \ + || { echo "fgj build has no $v verb — install the newest v*-j4k.* release" >&2; exit 1; } +done +``` + +The `--help` failure branch is the missing-or-broken-binary case — cobra answers `--help` with exit `0` even on a build with no `pr review` at all (the parent-fallback above), so a nonzero exit means the command never ran. Every other shape falls through to the per-verb check, whose message names the right remedy for all of them: the parent `pr` help of a stock or pre-`pr review` fork build, and even a `pr review` leaf command whose help prints no `Available Commands:` block at all — released j4k builds always print it (`pr review` shipped with `list` and `comments` already registered), but the probe no longer leans on that history to route the failure. The `exit 1` makes the snippet a hard gate for scripts; a workflow that can degrade instead — like the feedback-processing skill, which skips only the resolve step on a failed probe — runs the same probe and branches on its exit status rather than dying. + +Install the newest `v*-j4k.*` tag from the [releases page](https://codeberg.org/jercik/fgj/releases) — `scripts/install.sh` is checksum-verified for CI and local machines; in a Dockerfile, pull the `linux_{amd64,arm64}.tar.gz` release asset directly. Binaries are static and CGO-free, so they run anywhere (alpine, distroless, scratch). + +For Forge repos whose `origin` uses a transport-only host such as `code.tail.j4k.dev`, configure the API host with a Git alias instead of passing the SSH host as `--hostname`: + +```bash +fgj auth login --hostname code.j4k.dev --git-host code.tail.j4k.dev +``` + +Features are upstreamed one small PR at a time. Until a feature lands upstream and ships in a stock release, the j4k build is the source of truth. + +# Rule: Fix the Foundation First + +When you hit an issue likely to recur, stop and solve the underlying problem rather than working around it. The upfront investment pays for itself every time the issue would have resurfaced. + +For example: if you don't know how an API works and will need it repeatedly, don't guess at endpoints — build a tool that fetches and displays its docs. If a manual step keeps recurring, automate it. If knowledge is missing, capture it in a script, command, or doc so it's available next time. + +The key question: _"Will this come up again?"_ If yes, fix the root cause now. + +# Rule: Detect the Forge — GitHub (`gh`) vs Forgejo (`fgj`) + +Repos are mid-migration across two forges, so **never assume `gh`** — it only speaks GitHub's API. Detect the provider from `origin` before any forge operation (PRs, issues, CI/checks, releases, repo metadata, branch protection) and use the matching CLI. + +- **GitHub** — `github.com`, CLI `gh`, names untouched (`Jercik/j4k-cluster`). +- **The Forge** — self-hosted Forgejo at `code.j4k.dev`, CLI [`fgj`](https://codeberg.org/romaintb/fgj). Repos live under `j4k/` and drop the `j4k-` affix (`j4k-cluster` → `j4k/cluster`; an already-unprefixed repo like `setup-atlas` keeps its name). + +## Detection + +Run `npx -y repoq@latest forge --json`. The explicit tag prevents `npx` from reusing a stale cached release. It normalizes every URL shape (scp-like, `ssh://`, `https://`) and handles the `j4k-` affix that a hand-rolled `sed` gets wrong, returning `provider` (`github`/`forgejo`/`unknown`), `cli`, `slug`, `hostnameFlag`, `apiHost`, `apiBase`, plus `host`/`webHost`/`sshHost`/`owner`/`repo`. Reach for `apiHost` when passing a host to `fgj` — `hostnameFlag` is a two-word string that zsh, which does not word-split expansions, hands `fgj` as a single argument. Treat `unknown` as a hard stop. + +## Driving `fgj` + +`fgj` mirrors `gh`'s verbs (`pr`, `issue`, `release`, `repo`, `label`, `milestone`, with `--json` on the read verbs). For a PR's combined CI status, use `fgj pr checks ` (add `--json` for machine-readable output; exit codes match `gh pr checks` — `0` success/warning/skipped/no-checks, `1` failure/error, `8` pending). CI run history lives under `fgj actions run list|view|watch`; for step logs use `fgj actions run view --log` (also `--log-failed`, `-j `) — don't decompress the on-disk `actions_log/*.zst` chunks. + +When running inside the target repo, the j4k build of `fgj` can auto-detect the repo and API host. If `origin` uses a transport-only SSH host such as `ssh://git@code.tail.j4k.dev:2222/…`, configure that hostname as a `git_hosts` alias under the real API host (`code.j4k.dev`) and do not pass the SSH host as `--hostname`. + +For scripts, cross-repo commands, or work launched outside the target repo, use `repoq forge --json` and pass the returned host and slug explicitly: `fgj --hostname "$apiHost" -R "$slug" ...`. Explicit flags are still the most deterministic shape when cwd or remote config might not describe the target repo. The leading position is convention, not requirement — `--hostname` is a root persistent flag that cobra parses before or after the subcommand, so a trailing literal `--hostname ` (the `fgj auth` examples) is equally correct; the hazard the convention guards is the two-word `$hostnameFlag` expansion, which breaks at any position. One nuance, not an exception: the `auth` verbs declare a local `--hostname` that shadows the root flag, but cobra hands a leading flag to that local flag too, so both positions keep working — the difference is only that the shadowed root value never reaches the config fallback, and `auth login` has no fallback at all: pass its `--hostname` explicitly (either position) or it prompts interactively. + +## Beyond `fgj`'s verbs: `fgj api` + +Some operations have no dedicated verb. Reach for the generic `fgj api ` passthrough — it reuses the configured auth and resolved API host. The passthrough started in [the j4k custom build of `fgj`](./fgj-custom-build.md), which the other `fgj` rules already assume, and landed upstream in `v0.5.0` (2026-07); on an older stock `fgj` without it, fall back to `curl "$apiBase/"` with a token from `fgj auth token --hostname "$apiHost"` — capture it first and require it non-empty (`token=$(fgj auth token --hostname "$apiHost")`; an empty substitution would send an unauthenticated request whose `401` reads as an instance fault instead of a missing token), then pass it as a `curl` config on stdin — `printf 'header = "Authorization: token %s"\n' "$token" | curl -fsS --config - "$apiBase/"` — not as an `-H` argv header any process on the machine could read from the process table. + +- **Branch protection** (the Forge's replacement for GitHub "rulesets"): `/repos/{owner}/{repo}/branch_protections[/{name}]`. +- **A PR's combined CI status**: use the dedicated `fgj pr checks ` command instead — not the raw passthrough (add `--json` for machine-readable output; exit codes match `gh pr checks` — `0` success/warning/skipped/no-checks, `1` failure/error, `8` pending). +- **Combined commit status for an arbitrary commit not tied to a PR**: `/repos/{owner}/{repo}/commits/{sha}/status`. +- **Repo metadata as JSON**: `/repos/{owner}/{repo}` — or just `fgj repo view --json` (upstream since `v0.5.0`, alongside the passthrough). + +## Inspecting all Forgejo PR feedback + +Forgejo splits PR discussion across three API surfaces: issue comments at `/repos/{owner}/{repo}/issues/{number}/comments`, review summaries at `/repos/{owner}/{repo}/pulls/{number}/reviews`, and inline comments under each review at `/repos/{owner}/{repo}/pulls/{number}/reviews/{review-id}/comments`. Reading one surface misses the others: a review's summary body often carries only a count ("Found 2 medium issues") while the findings live in its inline comments, and responses land as issue comments. Web URLs anchor inline comments as `#issuecomment-`, but the API exposes them only under `/pulls/{number}/reviews/{review-id}/comments`, never in `/issues/{number}/comments`. + +Before handing off a PR, prove the review cycle is complete for the current head: + +1. Wait for the `PR Review` action run whose `commit_sha` equals `git rev-parse HEAD` (the run appears asynchronously — retry an empty result), then wait until every job in that run is terminal. A run-level failure or a terminal `fgj pr checks` does not mean all reviewer jobs finished. +2. Read the expected reviewer set from that run's jobs endpoint, not from workflow files in the checkout — `pull_request_target` executes the base branch's workflow. +3. Require every expected non-skipped reviewer job to have a submitted review whose `commit_id` is that head, and `fgj pr checks ` to report the same head in `.sha`. A reviewer job that exits without publishing a verdict blocks handoff — retry it. +4. Sweep all three surfaces (pagination rules below) and repeat until two consecutive sweeps return identical totals and records — a pending review can become visible without changing `X-Total-Count`. +5. After any follow-up push, redo the whole check against the new head; earlier reviews say nothing about the updated commit. + +Pagination on Forgejo 16.0.0: + +- **Reviews**: fetch `?limit=&page=N` for every page through `ceil(X-Total-Count / limit)`, taking the limit from `/settings/api` (`max_response_items`, 50 on code.j4k.dev). Don't stop at an underfilled page — Forgejo filters other users' pending reviews after database pagination, so a short page can precede later submitted reviews. The endpoint sends neither `Link` nor `X-HasMore`, so `fgj api --paginate` cannot prove completeness. Keep `PENDING` reviews in the sweep snapshot but don't treat their unpublished bodies or comments as feedback. A persistent gap between `X-Total-Count` and the deduplicated visible reviews is another user's unpublished draft — record it as a diagnostic, not a handoff blocker. +- **Issue comments**: one fetch returns everything — the endpoint ignores `page` and `limit`. Require the distinct returned IDs to match its `X-Total-Count`. + +`gh-feedback summary --json` aggregates issue comments, the inline review comments it reaches, and their reactions and responses — useful, but not proof of completeness: it omits review summary bodies and its Forgejo pager stops on an underfilled review page. Use it alongside the sweep, not instead of it. + +When processing or handing off a PR — not during a read-only audit — address every actionable finding and acknowledge the response on the PR before reporting completion. + +**Resolving conversations depends on the instance.** The j4k Forgejo fork adds a REST conversation-resolve API; stock Forgejo — upstream and codeberg.org, through 16.0.x — has none. Detect the capability from the `version` endpoint (`fgj --hostname "$apiHost" api version` — explicit host, or a probe run outside the target repo answers for `fgj`'s default host and reads that instance's capability instead; or `curl -fsS "$apiBase/version"` — `-f` so an HTTP error exits nonzero instead of handing the substring test an error body): the version string contains `-j4k` (live: `16.0.1-j4k.1+gitea-1.22.0`) on the fork, and `code.j4k.dev` qualifies — test for the substring, since build metadata (`+gitea-…`) trails the marker. The substring proves fork lineage, not the endpoint: an older `-j4k` server that predates the resolution route passes the test and `404`s on the first resolve — treat that like an unreadable version, skipping resolution for the pass and reporting it. A version without `-j4k` is stock Forgejo (e.g. codeberg.org's `16.0.0-dev-626-32363b81+gitea-1.22.0`); an unreadable version — `404`, network error, a body that isn't JSON with a `.version` string — blocks only resolution, not the feedback pass: process feedback normally, skip the resolve step, and report the failed probe rather than treating it as a stock verdict. This is the server's version — don't infer it from `fgj --version`, which reports the CLI build, a separate j4k fork that talks to stock servers just fine; that gate is effectively always true on this machine while the target instance may well be stock. Comment-minimize exists on neither build — Forgejo has no minimize concept at all. + +On a `-j4k` instance, `gh-feedback` v3.3.0+ owns native transitions for items it tracks; older builds use the feedback skill's qualified direct fallback. Raw-only workflows resolve with `fgj --hostname "$apiHost" pr review resolve -R "$slug"` and reopen with `… unresolve` (j4k `fgj` build `v0.5.0-j4k.4`+) — the explicit `-R` matters as much here as anywhere, since an omitted repo falls back to cwd detection. Any comment id in the thread works: the command lists the PR's reviews and their comments itself, walks to the thread's anchor, names the anchor it targeted, and reports the updated `resolver` (`--json` returns the updated anchor comment). Gate on the parent's subcommand listing per the j4k `fgj` build rule — on an older j4k build without the verb the call dies with cobra's `accepts 1 arg(s), received 3`, a usage error that means the verb is missing, not that you mistyped, and the fix is installing the newest build — not scripting a raw fallback; the REST path behind the verb goes deliberately unnamed here so it can't accrete one. Argument mistakes fail before any write with their own messages — `failed to list reviews` for a wrong PR number or slug, `not an inline review comment` for a review summary, issue comment, or foreign id (they aren't conversations) — and that second message also covers an inline id deleted since your sweep listed it, because the command re-lists every review comment before writing: re-run the sweep before reading it as your own mistyped id. A `404` from the resolve call itself means the server has no resolution API — stock Forgejo, or a `-j4k` server predating the route; the only deleted-id `404` is a comment vanishing in the instant between the command's own listing and its write. Token auth, same gate as other PR writes; success returns the updated anchor, whose `resolver` reflects real DB state and stays the original resolver on an idempotent re-resolve. + +**Verification still needs the conversation partition.** The resolve endpoint writes `resolver` to exactly the comment it is handed, and the UI reads a conversation's resolved state only from its anchor — that is why the command walks to the anchor before writing, and why the completeness sweep must read each anchor's `resolver` rather than any reply's. The API exposes no threading field, so derive conversations from the sweep's inline comments: a conversation is the code comments sharing a `path`, a side with its display line, and a `pull_request_review_id` — replies join the review they answer, while comments that different reviews leave at the same line are separate conversations, each resolved on its own. The side is whichever of `position` (new side) / `original_position` (old side) is nonzero — the unused side reports `0`, and old-side and new-side comments at the same number are distinct conversations; both at `0` is the stored line-`0` edge (the server keeps one signed line and reports it on a single side), which groups like any other value — same review, same path, line `0`, one conversation — and the display line is that number plus `extra_lines_count` — upstream Forgejo API since `v16.0.0`, where multi-line comments landed, not a fork field: Forgejo buckets a multi-line comment at the _end_ of its range, so a comment spanning 55–60 and a single-line comment at 60 from the same review are one conversation, and grouping by the raw `position` pair alone splits them and mis-picks the anchor. The anchor is the conversation's earliest comment (`created_at`, ties by lowest `id`). This partition is the one the API listings and the Conversations tab render; the sweep's `position` is the line as of the comment's own commit, while the diff view re-blames comments to the current head and can merge same-review buckets after lines move — don't expect it to mirror the derivation. `gh-feedback` derives this same native partition when reading or changing resolver state, but its feedback items still thread by reply markers: an item id is therefore not necessarily the anchor, and two findings one review left at the same display line are separate items sharing one conversation whose single resolve state — the anchor's, per the write-where-handed endpoint above, so a sibling root comment can carry a stray `resolver` from a direct write without changing what renders — speaks for both. Confirm the anchor's `resolver` is set after resolving — the per-review comment listings the sweep already fetches return it on every comment — and read it first to know the current state before unresolving. + +On a stock instance no resolve endpoint exists — the review `dismissals` endpoint dismisses a review's verdict, not a conversation, and the web "Resolve conversation" route authenticates by browser session cookie only, so a token POST 303-redirects to `/user/login` and resolves nothing. Track "done" by reaction there instead of pretending to resolve. + +# Rule: Generated Agent-File Drift Is Expected + +Automated tooling rewrites tracked agent instruction files (`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md`) whenever shared rules change, so these files may sit modified for long stretches. + +When they are the only modifications, treat the repository as clean — don't stash, revert, or delete the changes, and don't commit them on their own. Include them in the next substantive commit or pull request instead. + +# Rule: Local Third-Party Checkouts + +Before searching the web or relying on memory for an external library, check `~/Developer/third-party/` — it holds dozens of third-party repos cloned locally for reference. The source is faster and more authoritative than any secondary description. + +Run `ls ~/Developer/third-party/` to see what's available, then explore as with any local code. Don't clone new repos into this directory unsolicited — the user curates it. + +# Rule: Target Bash 3.2 + +Unless the user specifies otherwise, write shell scripts for bash 3.2 — the default `/bin/bash` on macOS. + +When using `printf`, do not pass a literal string starting with `-` as the format string. Use `printf '%s\n' "$value"` or `echo` so Bash 3.2 does not parse the marker as an option. + +Backslash line continuation inside a `for … in` word list across multiple lines is fragile in Bash 3.2 and can fail with "syntax error near unexpected token `done`". Collect the patterns in an array (`globs=(g1 g2 g3)`) and iterate with a nested loop, or put all patterns on a single line. + +# Rule: No Backwards Compatibility + +Prioritize the best shape of the current codebase over backwards compatibility. When something changes, change it completely — rename the function, delete the old flag, stop reading the old config path. Let the code reflect what it _is_, not what it _was_; version control remembers what was there. + +Remove every form of backwards-compatibility ballast: + +- Aliases and re-exports keeping old import paths or type names working +- Fallback reads from old config locations or env var names +- Renamed-but-kept flags, options, or CLI arguments +- Feature flags gating already-shipped changes +- Underscore-prefixed "unused" variables kept to silence lint +- `// removed: …` or `// TODO: delete in v3` comments marking absent code +- Deprecated types re-exported from their old module +- Database migrations committed before the project has real users — reset the dev DB and iterate + +Compatibility code accumulates as dead weight: it obscures intent, inflates surface area, and forces every future reader to reason about states that no longer exist. + +## Database migrations + +Committed migrations are permanent — every schema tweak becomes a file that travels with the project forever, even if the only "old" schema lived for an afternoon in a dev DB. Before a deployment holds data you can't discard, reset the dev DB and iterate on the schema directly; commit only the current state. A squashed clean initial schema is far cheaper to reason about than a chain of pre-release churn. + +Once real users have real data, the mode flips: every schema change becomes a migration, no exceptions. The cutover is "first deployment with persistent real data," not "first commit" or "first merged PR." The same reasoning extends to any on-disk or over-the-wire format you cannot discard — serialized caches, stored session blobs, published API shapes — iterate freely before the first real reader exists, then lock it down. + +# Rule: Access OrbStack Machines with `orb`, Not SSH + +To run a command inside an OrbStack Linux machine, use `orb -m -u ` (wrap in `bash -lc '…'` when you need a login shell, PATH, or pipes). The standing dev VM is `debian` with user `j4k`: + +```bash +orb -m debian -u j4k bash -lc 'node --version' +``` + +Don't reach for `ssh @.orb.local` — it authenticates by public key and fails with `Permission denied (publickey)` unless that user already has your key in `authorized_keys`. `orb` reuses OrbStack's host identity mapping, so it needs no key and works for any existing user on the machine. + +# Rule: Package Manager Execution + +How different package manager commands resolve binaries: + +| Command | Behavior | +| ----------------- | ----------------------------------------------------------------------- | +| `pnpm exec foo` | Runs from `./node_modules/.bin`; falls back to system PATH | +| `pnpx foo` | Always fetches from registry (uses dlx cache); ignores local installs | +| `npx foo` | Checks local `node_modules/.bin` → global → downloads from registry | +| `npx foo@version` | Resolves version, uses local if exact match exists, otherwise downloads | + +`pnpx` is an alias for `pnpm dlx`. + +# Rule: Prefer OrbStack Locally + +Use OrbStack as the local container and Linux VM runtime on macOS — not Docker Desktop, Colima, or a Podman machine. The `docker` and `docker compose` CLIs work unchanged; `orbctl` (aliased `orb`) creates and manages full Linux VMs. + +OrbStack has faster cold starts, lower idle CPU and memory, native macOS file sharing without bind-mount workarounds, and a single tool for both containers and VMs. Assume any container or VM workflow on this host runs through OrbStack. + +For ad hoc Linux VM testing, see the `orbstack-ad-hoc-vm` skill. + +# Rule: Prefer TypeScript Over Python + +When writing new code and the user states no language requirement, default to TypeScript. This yields to explicit user input: write Python when the user asks, when the task lives in a Python codebase, or when the ecosystem forces it (data science, ML, a Python-only library). + +# Rule: Project Skill Symlinks + +Keep the real skill in `.agents/skills//` and treat `.claude/skills/` as a symlink to it, not a second copy. Create the Claude entry with `ln -s ../../.agents/skills/ .claude/skills/` so both locations point at the same source of truth. + +# Rule: Runtime Tool Discovery + +When a workflow references a custom CLI — a local script or anything you may not already know — run ` --help` before first use. The help output is the authoritative source for subcommands, flags, and usage; rules only name the tool and rely on `--help` to teach you the rest at runtime. + +Compose these tools with pipes like any Unix CLI, and prefer machine-readable formats (`--porcelain`, `--json`) over parsing human-readable output. + +# Rule: Repository Scripts + +`scripts/` is the default home for the repository's operational tooling — automation, helpers, and one-offs that maintain the repo but aren't part of what it ships. + +**Look there first.** If `scripts/` exists, `ls scripts/` and `jq '.scripts' package.json` before writing anything new — extend what's there rather than forking it. + +**Put new scripts there by default.** Anything worth committing — release helpers, submodule updates, git hooks, recurring chores — goes in `scripts/`, wired through `package.json` so callers invoke it by name rather than remembering the path. + +# Rule: Set an Explicit Timeout for Long CI Waits + +Waiting on CI to finish — `gh run watch`, `fgj actions run watch`, or a poll loop over `gh run list --commit ` — routinely outlasts an agent shell tool's default command timeout (Claude Code's Bash tool defaults to 120s, raisable to 600s). Pass an explicit longer timeout (e.g. 420000 ms) to the command invocation, or the wait is killed mid-run and reports a false failure. + +# Rule: Sub-Agent Delegation + +Spawn sub-agents liberally. A sub-agent encapsulates a chunk of work behind a simple interface: briefed on the task, it gathers its own context, works autonomously, and hands back only the result — trust the process and engage with the outcome. The main agent's job is to frame tasks, dispatch, and synthesize results. + +**Delegate, move on, verify.** A task of many simple steps is prime delegation material — deploying an Ansible playbook and combing its verbose logs where nearly every task just reports ok, provisioning a throwaway OrbStack VM with Node, Docker, and Postgres installed, clicking through a multi-page web flow filling fields and pressing buttons, watching a CI run, applying a bulk mechanical edit. Hand it to a background sub-agent, move on to other work while it runs, and when it finishes, spawn a fresh sub-agent to confirm the work was done correctly — and for critical work, several adversarial reviewers, each attacking the result through a different lens. + +**Skills delegate too.** Instead of invoking a skill yourself, consider handing it to a sub-agent: name the skill and the inputs, and the sub-agent loads it, follows the workflow, and returns the result — the skill's full instruction set never enters the main context. `agent-browser` is the perfect shape for this: "log in to the site and check that such-and-such feature works" is a one-line instruction with a one-line answer, and everything in between — dozens of tool calls, failed selectors, retries — stays encapsulated in the sub-agent. Whether that fits is a per-skill call. + +# Rule: TSV Parsing + +`awk` splits on any whitespace by default, silently breaking on TSV values containing spaces. For tab-separated output (often `--porcelain` flags), set the delimiter explicitly: + +```bash +# BAD: prints "name" instead of "name with spaces" +printf 'id\tname with spaces\tstatus\n' | awk '{ print $2 }' + +# GOOD — pick one: +awk -F'\t' '{ print $2 }' +cut -f2 # cut defaults to tab +while IFS=$'\t' read -r a b c; do …; done +``` + +Empty fields are a second trap for the `read` form only: tab is IFS _whitespace_, so runs of tabs collapse into one delimiter and an empty middle field shifts every value after it — `awk -F'\t'` and `cut -f` are immune. When a field can be empty, prefer `awk`/`cut`, keep nullable fields last, or in zsh double the tab (`IFS=$'\t\t'`), which the manual defines as demoting it to a hard delimiter that preserves empty fields; bash has no doubled form — it silently ignores the doubling and shifts the fields anyway, so a bash-run copy of the zsh idiom reinstates the exact bug it exists to prevent, with no diagnostic. + +Not every `--porcelain` is TSV — `git worktree list --porcelain` is space-separated per line. Sample output before picking a delimiter. + +# Rule: Reach for Unix-Native Primitives Before Inventing Abstractions + +Use the OS as the first control plane. Before proposing a registry, supervisor, scheduler, logger, IPC layer, config store, or discovery protocol, check whether argv, environment variables, inherited file descriptors, filesystem paths, Unix-domain sockets, ports, signals, stdout/stderr, cron, systemd, or XDG paths already solve it. + +Two primitives cover almost everything: + +- **Handoff at fork/exec** — parent passes addresses to children via args, env vars, or inherited FDs (`SSH_AUTH_SOCK`, systemd socket activation). +- **Well-known names in a shared namespace** — filesystem paths and TCP/UDP ports (`/var/run/docker.sock`, port 22). The filesystem is the service directory. + +Common problems map directly: process discovery → socket at a conventional path or env var; IPC → Unix-domain socket, named pipe, or signal; supervision → systemd or another init; scheduling → cron or systemd timer; logging → stdout/stderr; config → XDG config dir. + +For Node apps needing a config, data, cache, log, or temp directory, default to [`env-paths`](https://github.com/sindresorhus/env-paths). It returns the right location per platform — XDG on Linux, `~/Library/...` on macOS, `%APPDATA%` on Windows — so you don't hand-roll `process.platform` branches that drift. Pass a namespace (`envPaths('my-app')`) and use the returned `config`, `data`, `cache`, `log`, `temp` paths directly. + +**Don't build a second control plane.** Reject the native primitive only when you can name the concrete property it cannot provide: distributed discovery across hosts, authorization the OS namespace cannot enforce, schema evolution for a long-lived wire format, multiplexing many streams over one transport, binary streaming with backpressure, or cross-platform targets where no equivalent primitive exists everywhere. + +Otherwise the native primitive wins. Designs that don't compose with pipes, signals, and conventional file locations pay a tax forever. + +# Rule: Use Native TypeScript Execution + +Use Node 24+ and run `.ts` files directly with `node script.ts`. Node strips types at runtime — no `tsx`, no `ts-node`, no `tsc` build step. + +Default to `.ts` over `.mjs` for new scripts and to `node` over `tsx` in `package.json`. + +# Rule: Delegate Through Subagents by Default + +Act as an orchestrator: frame the work, dispatch subagents, synthesize results, and report to the user. Push discovery and execution into subagents via the `task` tool so the main thread stays focused on decisions and synthesis rather than raw search output or exploratory dead ends. + +Delegate eagerly — more than your conservative guidance around `explore` suggests. If work is moderately non-trivial, multi-file, multi-step, cross-cutting, or would take more than a short search/read cycle, delegate immediately. Use `explore` early for discovery, flow tracing, or parallel investigation. When work splits cleanly, run subagents in parallel. + +## Launch Defaults + +- Use `agent_type: "general-purpose"` and `model: "gpt-5.5"` by default when delegating substantial work. State any model downgrade explicitly before launching. +- Prefer `mode: "background"` when work can proceed in parallel; use `mode: "sync"` only when you need the result to continue. +- Retrieve results from background agents with `read_agent`. +- If this environment does not expose an agent follow-up tool, launch a new subagent with the refined prompt instead of assuming one exists. + +## Concurrency Limits + +Run at most four subagents in parallel. When more work is ready than slots available, hold the surplus in an explicit queue and launch the next one only after a running subagent finishes. Track pending prompts so nothing is dropped when a slot opens. + +## Applying This Rule + +- A skill's workflow does **not** disable delegation by itself. If the task is multi-file, multi-step, cross-cutting, or naturally partitioned by agent/module/topic, delegate first unless the skill explicitly forbids it. +- Before manual exploration of any non-trivial task, stop and decide whether the work can be split into subagents. If yes, launch them before doing the exploration in the main thread. + +## Prompting Subagents + +- Give each subagent a clear objective and expected output, and instruct it to do the work itself rather than advise. +- Anchor prompts with paths, directories, symbol names, error messages, or feature names you already have. +- Batch related work into one subagent; split independent work into parallel subagents. + +## Patience and Monitoring + +Once a subagent is running, let it finish unless it is clearly stuck. Check progress with `read_agent`; if refinement is needed and no follow-up agent tool exists, start a new subagent with the corrected prompt. + +# Rule: Use `ask_user` for Questions and Handoffs + +`ask_user` has two jobs: asking explicit questions and handing control back to the user. Call it for both — never ask in prose, and never end a turn with prose alone when control returns to the user. + +Before asking a question, resolve anything that does not require user judgement: search the codebase, read docs, run experiments, trace behavior. Reserve `ask_user` for missing constraints, preferences, scope boundaries, destructive-action approval, blocked decisions, or the next instruction when no task remains. + +**No prose-only handoffs.** Whenever you stop acting autonomously, `ask_user` must be the final action. After completing or blocking on work, give a one- or two-sentence outcome update, then call `ask_user` in the same response. Don't end with plain text that merely acknowledges, summarizes, confirms, reports completion, asks what to do next, or waits — a polished completion summary is still a handoff. + +An `ask_user` answer is just another user turn: handle it, and if you hand control back again, end with `ask_user` again. The rule applies after greeting-only turns, completed tasks, blocked tasks, confirmations following an `ask_user` selection, and "what should I do next?" prompts. It still applies when the task feels complete or the user says "stop" or "we're done" — if the user truly wants to end the session, they can terminate it. + +# Rule: Work Autonomously + +Default to sustained, end-to-end execution. Run the full cycle — explore → plan → implement → validate — without pausing for user input between steps. + +- When facing ambiguity, pick the most reasonable interpretation and proceed; state your assumptions so the user can correct after the fact. +- Don't stop after planning — implement. Don't stop after implementing — validate. Don't stop after one fix — check for related issues. +- When something fails, diagnose and retry with a different approach before surfacing to the user. diff --git a/archive/global-targets/gemini-AGENTS.md b/archive/global-targets/gemini-AGENTS.md new file mode 100644 index 0000000..3453a85 --- /dev/null +++ b/archive/global-targets/gemini-AGENTS.md @@ -0,0 +1,377 @@ +# Rule: 1Password Commit Signing + +This machine signs git commits via 1Password. Any signing error during `git commit` — 1Password socket errors, "failed to sign the data", "fatal: failed to write commit object" — usually means 1Password is locked. Ask the user to unlock 1Password, then retry the commit. + +# Rule: `AGENTS.md` Is Generated — Edit the Source + +`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md` and the like are generated by the `sync-rules` CLI. The next `sync-rules` run overwrites direct edits. + +To change what an agent reads, edit one of two sources: + +- **Rule content** in `~/Developer/j4k/setup-atlas/rules//*.md` — reword an existing file or add a new one under the appropriate category. +- **Which rules apply to which repo** in `/Users/jercik/Library/Preferences/sync-rules/config.json` — add or remove a glob (e.g., `"nodejs/*.md"`) under a `projects[].path` entry, or under `global` / `globalOverrides` for cross-repo defaults. + +Run `sync-rules` after either edit. + +# Rule: Automatic Repository Alignment Scope + +When the user asks to align or audit "all my repositories", that means the repository checkouts directly under `~/Developer/jercik/`, `~/Developer/j4k/`, and `~/Developer/j4k-oss/` — nothing outside those three directories. + +# Rule: Canonical Repository Checkout Layout + +Place every owned repository under `~/Developer//`, using the remote owner and repository slugs rather than a locally invented prefix: + +- GitHub `Jercik/example` → `~/Developer/jercik/example` +- GitHub `validationcloud/example` → `~/Developer/validationcloud/example` +- Forgejo `j4k/example` → `~/Developer/j4k/example` +- Forgejo `j4k-oss/example` → `~/Developer/j4k-oss/example` + +Use the remote repository name verbatim. A historical local prefix is not part of the name: Forgejo `j4k/align` belongs at `~/Developer/j4k/align`, never `~/Developer/j4k-align`. + +Git worktrees live in the same owner directory as the main checkout, with the branch name appended to the directory name as `-`: a worktree for branch `foo` of `~/Developer/j4k/align` goes at `~/Developer/j4k/align-foo`. Create worktrees with `worktree-add ` from inside the repository — it places the new checkout at that canonical path automatically, copies useful local files, and installs dependencies. + +Before cloning, resolve the forge and owner/repository slug, create the owner directory, and pass the canonical destination explicitly. The account-aware `gh` shim selects the ValidationCloud account and SSH key for `validationcloud/*` and `lukasz-jercinski-vc/*` targets, and the personal account otherwise. Forgejo clones use the canonical tailnet SSH transport and an explicit destination. `code.tail.j4k.dev` is reachable only while the machine is connected to the Tailscale network — a clone or fetch that hangs or can't connect usually means the tailnet is down, not that the key or remote is wrong; check the Tailscale connection before debugging SSH. + +Third-party source remains under `~/Developer/third-party/` and is never reorganized by owner. Do not infer an owner for a Git root with no remote or for a plain local directory; leave it in place until the user classifies it. + +# Rule: You Share This Workspace + +Other agents and the user may have uncommitted WIP in the working tree, and new changes can appear mid-session. Don't assume unexpected state came from your edits, and don't stash, overwrite, or commit work you didn't make — even stash-then-pop can confuse another agent whose tree state shifts underfoot. + +If you need an isolated tree, ask the user about creating a git worktree and move your changes there. + +# Rule: Conventional Commits + +Write every git commit message and pull request title in Conventional Commits format (`type: subject`). + +Before authoring a PR title and body, load the `pr-writing-style` skill if it is installed — it owns the prose: title wording, body shape, what gets cut. + +# Rule: Create PRs on the Repo's Forge (No Shell Expansion) + +Detect the forge first — [the forge detection rule](./forge-provider-detection.md) owns the how — and use the matching PR tool: `gh` for GitHub, `fgj` for Forgejo. Don't reach for `gh` reflexively; it can't open a PR on Forgejo. + +The shared trap is shell expansion of multi-line Markdown bodies. A double-quoted body string lets the shell expand backticks and `$...` before the CLI sees it, mangling code blocks and variable references. Pass the body so the shell never scans it. + +**GitHub (`gh`)** — `--body-file` with a single-quoted heredoc (`'EOF'` disables all expansion): + +```bash +gh pr create --title "docs: clarify example" --body-file - <<'EOF' +Clarifies that the `..${sep}` check doesn't block `..foo/bar.txt`. +EOF +``` + +**Forge (`fgj`)** — `fgj pr create` takes only `-b` (no `--body-file`), so write the body with a single-quoted heredoc, then pass it by command substitution. The file's bytes become the argument verbatim; the shell does not re-scan them for expansion: + +```bash +forge=$(mktemp) +npx -y repoq@latest forge --json > "$forge" +apiHost=$(jq -r '.apiHost' "$forge") # e.g. code.j4k.dev, codeberg.org +slug=$(jq -r '.slug' "$forge") +body=$(mktemp) +cat > "$body" <<'EOF' +Clarifies that the `..${sep}` check doesn't block `..foo/bar.txt`. +EOF +fgj --hostname "$apiHost" pr create -R "$slug" \ + --title "docs: clarify example" --base main --head my-branch \ + -b "$(cat "$body")" +rm -f "$body" "$forge" +``` + +When running from the target repo with the j4k build configured, `fgj` can auto-detect the repo and API host, including `git_hosts` aliases for SSH-only transport hosts. For scripts, cross-repo work, or commands launched outside the target repo, still pass `--hostname "$apiHost"` and `-R "$slug"` from `repoq forge --json`; explicit flags remove cwd and remote-alias ambiguity. `fgj pr edit -F ` accepts a body file if you'd rather create then edit. + +Verify with the matching CLI: `gh pr view --json title,body,url` or `fgj --hostname "$apiHost" pr view -R "$slug" --json`. + +# Rule: Doc-Comments and Example Snippets Are Documentation + +Audit JSDoc/docstrings and example code in source with the same rigor as markdown. Example calls must type-check against the current signature; a required `T | undefined` field omitted from an example is drift even though it "looks" fine. + +# Rule: Encode Cross-Repo Config Learnings in j4k-align + +`j4k-align` (in `~/Developer/j4k/align/`) audits and aligns repo settings on either forge, automation inputs, workflows, rulesets (GitHub) / branch protection (Forgejo), and managed files across every repo. Its checks and templates are the source of truth for cross-repo configuration — on-disk state drifts; the audit defines what "correct" means. + +j4k-align detects the forge per repo from its git origin and audits and aligns config across both GitHub (via `gh`) and the self-hosted Forgejo instance (`code.j4k.dev`, via `fgj`) end to end — there is no Forge gap to work around. + +When you hit a config issue in one repo — a misbehaving lint rule, a missing tsconfig field, a workflow that breaks on a new dependency — ask: **"Will this come up again in another repo?"** If yes, the fix belongs in `j4k-align`. Patching one repo lets the same trap resurface elsewhere; encoding it in the audit catches it everywhere. + +## Traits, triggers, and the extension loop + +Repos are classified by **traits** — labels like `pnpm-package`, `react`, `nextjs`, `external-references`, `private-oci-publish` defined in `src/alignment/schemas.ts`. Each trait is assigned by a **trigger** — a signal from the repo (file presence, `package.json` dependency, `.gitmodules` content, registry config). Traits gate checks and template selection. + +Touchpoints when extending the audit: + +- Trigger: `src/traits/signals.ts` +- Trait assignment: `src/traits/build-traits.ts` (enum in `src/alignment/schemas.ts`) +- Trait-gated check: `src/cli/verify/check-*.ts`, wired in `src/cli/verify/run-checks.ts` +- Fix implementation: `src/checks/*.ts`, wired in `src/cli/fix/local-project-configs/fix-local-tool-configs.ts` +- Templates: `templates/`, with `TraitSelection` guards in `src/resolve-template-files.selections.ts` + +**Fix the signal, not the symptom.** If a repo should have a trait but doesn't — or has one it shouldn't — the trigger is wrong. Repair `src/traits/signals.ts` rather than hardcoding the trait downstream. Verify with `j4k-align --repo owner/name`, then `--fix` to confirm remediation. + +## Forbid the trap, not just the current case + +When you hit a known footgun — a setting that silently breaks types, a flag that disables a guarantee, a path pattern that traps on edge cases — add a check that rejects that value across every applicable trait, not one that only repairs the current repo. Forbid the specific bad shape, name the check clearly, and record the failure mode it prevents inside the check so the reasoning travels with the code. + +## Scope + +j4k-align governs cross-repo configuration: build, lint, format, tsconfig, workflows, rulesets (GitHub) / branch protection (Forgejo), repo settings on either forge, automation secrets and variables, and managed files (git hooks, `release.config.mjs`, etc.). Project-specific business code, product schema, and feature behavior stay in their repos. + +## Anti-patterns + +- `eslint-disable`, `// @ts-ignore`, or `.gitignore` entries that silence a warning other repos will hit identically. +- Hand-editing `.github/workflows/*.yml` or `.forgejo/workflows/*.yml` when the source-of-truth template lives in `templates/`. +- Diverging a per-repo `tsconfig.json` to dodge a check instead of fixing the check. +- Hardcoding a trait or skipping a check when the real bug is in the trigger. + +This is a specialized application of _Fix the Foundation First_ — the foundation here is the trait/trigger/check/fix pipeline. + +# Rule: `eslint-config-axkit` Is Deprecated + +Superseded by oxlint with `@j4k/oxlint-config`; it cannot run under the fleet's `typescript@^7` pin, which breaks `typescript-eslint`. Never add it to a project — when touching a repo that still lints through it, migrate to oxlint instead. + +# Rule: Use the j4k Custom Build of `fgj` + +The j4k fork at [`codeberg.org/jercik/fgj`](https://codeberg.org/jercik/fgj) ships features not yet in upstream `romaintb/fgj` — `fgj pr review` with inline comments, `fgj pr review resolve`/`unresolve` for conversation resolution (against the j4k Forgejo fork's resolution API), `fgj pr checks` for a PR's combined CI status, the `fgj pr list --base`/`--head` server-side filters, org-scoped `fgj actions` secrets/variables (with pipeable secret input), and `git_hosts` aliases for remotes whose SSH host differs from the Forgejo API host. Features leave this list as they merge: the generic `fgj api` passthrough and `fgj repo view --json` landed upstream in `v0.5.0` (2026-07). Other rules here lean on these commands, so prefer this build over the stock Homebrew one. + +**Check the build, not the version number.** The `-j4k.N` suffix on `fgj --version` is the durable tell; a bare upstream version (e.g. `0.4.0`) means stock. Gate on the suffix, which survives version bumps: + +```bash +fgj --version | grep -q j4k || echo "stock fgj — install the j4k build" +``` + +The suffix only proves the fork lineage, not any one feature — features accrete across `-j4k.N` releases, so an older fork build passes the gate while missing a newer verb. When a workflow depends on a specific command, gate on the parent's subcommand listing. An exit-code probe (`fgj pr review resolve --help`) false-passes on the build it exists to catch: cobra reads the unknown word as a positional argument and answers `--help` with the parent's help, exit `0`. The listing discriminates — scoped to the `Available Commands:` block, so an indented prose or example line that happens to start with a verb name can't satisfy it — and checks every verb the workflow needs, since the accretion argument above applies verb by verb: + +```bash +help=$(fgj pr review --help) \ + || { echo "fgj pr review --help failed — is fgj installed?" >&2; exit 1; } +verbs=$(awk '/^Available Commands:/{f=1;next} /^[^[:space:]]/{f=0} f' <<<"$help") +for v in resolve unresolve; do + grep -qE "^[[:space:]]+$v([[:space:]]|$)" <<<"$verbs" \ + || { echo "fgj build has no $v verb — install the newest v*-j4k.* release" >&2; exit 1; } +done +``` + +The `--help` failure branch is the missing-or-broken-binary case — cobra answers `--help` with exit `0` even on a build with no `pr review` at all (the parent-fallback above), so a nonzero exit means the command never ran. Every other shape falls through to the per-verb check, whose message names the right remedy for all of them: the parent `pr` help of a stock or pre-`pr review` fork build, and even a `pr review` leaf command whose help prints no `Available Commands:` block at all — released j4k builds always print it (`pr review` shipped with `list` and `comments` already registered), but the probe no longer leans on that history to route the failure. The `exit 1` makes the snippet a hard gate for scripts; a workflow that can degrade instead — like the feedback-processing skill, which skips only the resolve step on a failed probe — runs the same probe and branches on its exit status rather than dying. + +Install the newest `v*-j4k.*` tag from the [releases page](https://codeberg.org/jercik/fgj/releases) — `scripts/install.sh` is checksum-verified for CI and local machines; in a Dockerfile, pull the `linux_{amd64,arm64}.tar.gz` release asset directly. Binaries are static and CGO-free, so they run anywhere (alpine, distroless, scratch). + +For Forge repos whose `origin` uses a transport-only host such as `code.tail.j4k.dev`, configure the API host with a Git alias instead of passing the SSH host as `--hostname`: + +```bash +fgj auth login --hostname code.j4k.dev --git-host code.tail.j4k.dev +``` + +Features are upstreamed one small PR at a time. Until a feature lands upstream and ships in a stock release, the j4k build is the source of truth. + +# Rule: Fix the Foundation First + +When you hit an issue likely to recur, stop and solve the underlying problem rather than working around it. The upfront investment pays for itself every time the issue would have resurfaced. + +For example: if you don't know how an API works and will need it repeatedly, don't guess at endpoints — build a tool that fetches and displays its docs. If a manual step keeps recurring, automate it. If knowledge is missing, capture it in a script, command, or doc so it's available next time. + +The key question: _"Will this come up again?"_ If yes, fix the root cause now. + +# Rule: Detect the Forge — GitHub (`gh`) vs Forgejo (`fgj`) + +Repos are mid-migration across two forges, so **never assume `gh`** — it only speaks GitHub's API. Detect the provider from `origin` before any forge operation (PRs, issues, CI/checks, releases, repo metadata, branch protection) and use the matching CLI. + +- **GitHub** — `github.com`, CLI `gh`, names untouched (`Jercik/j4k-cluster`). +- **The Forge** — self-hosted Forgejo at `code.j4k.dev`, CLI [`fgj`](https://codeberg.org/romaintb/fgj). Repos live under `j4k/` and drop the `j4k-` affix (`j4k-cluster` → `j4k/cluster`; an already-unprefixed repo like `setup-atlas` keeps its name). + +## Detection + +Run `npx -y repoq@latest forge --json`. The explicit tag prevents `npx` from reusing a stale cached release. It normalizes every URL shape (scp-like, `ssh://`, `https://`) and handles the `j4k-` affix that a hand-rolled `sed` gets wrong, returning `provider` (`github`/`forgejo`/`unknown`), `cli`, `slug`, `hostnameFlag`, `apiHost`, `apiBase`, plus `host`/`webHost`/`sshHost`/`owner`/`repo`. Reach for `apiHost` when passing a host to `fgj` — `hostnameFlag` is a two-word string that zsh, which does not word-split expansions, hands `fgj` as a single argument. Treat `unknown` as a hard stop. + +## Driving `fgj` + +`fgj` mirrors `gh`'s verbs (`pr`, `issue`, `release`, `repo`, `label`, `milestone`, with `--json` on the read verbs). For a PR's combined CI status, use `fgj pr checks ` (add `--json` for machine-readable output; exit codes match `gh pr checks` — `0` success/warning/skipped/no-checks, `1` failure/error, `8` pending). CI run history lives under `fgj actions run list|view|watch`; for step logs use `fgj actions run view --log` (also `--log-failed`, `-j `) — don't decompress the on-disk `actions_log/*.zst` chunks. + +When running inside the target repo, the j4k build of `fgj` can auto-detect the repo and API host. If `origin` uses a transport-only SSH host such as `ssh://git@code.tail.j4k.dev:2222/…`, configure that hostname as a `git_hosts` alias under the real API host (`code.j4k.dev`) and do not pass the SSH host as `--hostname`. + +For scripts, cross-repo commands, or work launched outside the target repo, use `repoq forge --json` and pass the returned host and slug explicitly: `fgj --hostname "$apiHost" -R "$slug" ...`. Explicit flags are still the most deterministic shape when cwd or remote config might not describe the target repo. The leading position is convention, not requirement — `--hostname` is a root persistent flag that cobra parses before or after the subcommand, so a trailing literal `--hostname ` (the `fgj auth` examples) is equally correct; the hazard the convention guards is the two-word `$hostnameFlag` expansion, which breaks at any position. One nuance, not an exception: the `auth` verbs declare a local `--hostname` that shadows the root flag, but cobra hands a leading flag to that local flag too, so both positions keep working — the difference is only that the shadowed root value never reaches the config fallback, and `auth login` has no fallback at all: pass its `--hostname` explicitly (either position) or it prompts interactively. + +## Beyond `fgj`'s verbs: `fgj api` + +Some operations have no dedicated verb. Reach for the generic `fgj api ` passthrough — it reuses the configured auth and resolved API host. The passthrough started in [the j4k custom build of `fgj`](./fgj-custom-build.md), which the other `fgj` rules already assume, and landed upstream in `v0.5.0` (2026-07); on an older stock `fgj` without it, fall back to `curl "$apiBase/"` with a token from `fgj auth token --hostname "$apiHost"` — capture it first and require it non-empty (`token=$(fgj auth token --hostname "$apiHost")`; an empty substitution would send an unauthenticated request whose `401` reads as an instance fault instead of a missing token), then pass it as a `curl` config on stdin — `printf 'header = "Authorization: token %s"\n' "$token" | curl -fsS --config - "$apiBase/"` — not as an `-H` argv header any process on the machine could read from the process table. + +- **Branch protection** (the Forge's replacement for GitHub "rulesets"): `/repos/{owner}/{repo}/branch_protections[/{name}]`. +- **A PR's combined CI status**: use the dedicated `fgj pr checks ` command instead — not the raw passthrough (add `--json` for machine-readable output; exit codes match `gh pr checks` — `0` success/warning/skipped/no-checks, `1` failure/error, `8` pending). +- **Combined commit status for an arbitrary commit not tied to a PR**: `/repos/{owner}/{repo}/commits/{sha}/status`. +- **Repo metadata as JSON**: `/repos/{owner}/{repo}` — or just `fgj repo view --json` (upstream since `v0.5.0`, alongside the passthrough). + +## Inspecting all Forgejo PR feedback + +Forgejo splits PR discussion across three API surfaces: issue comments at `/repos/{owner}/{repo}/issues/{number}/comments`, review summaries at `/repos/{owner}/{repo}/pulls/{number}/reviews`, and inline comments under each review at `/repos/{owner}/{repo}/pulls/{number}/reviews/{review-id}/comments`. Reading one surface misses the others: a review's summary body often carries only a count ("Found 2 medium issues") while the findings live in its inline comments, and responses land as issue comments. Web URLs anchor inline comments as `#issuecomment-`, but the API exposes them only under `/pulls/{number}/reviews/{review-id}/comments`, never in `/issues/{number}/comments`. + +Before handing off a PR, prove the review cycle is complete for the current head: + +1. Wait for the `PR Review` action run whose `commit_sha` equals `git rev-parse HEAD` (the run appears asynchronously — retry an empty result), then wait until every job in that run is terminal. A run-level failure or a terminal `fgj pr checks` does not mean all reviewer jobs finished. +2. Read the expected reviewer set from that run's jobs endpoint, not from workflow files in the checkout — `pull_request_target` executes the base branch's workflow. +3. Require every expected non-skipped reviewer job to have a submitted review whose `commit_id` is that head, and `fgj pr checks ` to report the same head in `.sha`. A reviewer job that exits without publishing a verdict blocks handoff — retry it. +4. Sweep all three surfaces (pagination rules below) and repeat until two consecutive sweeps return identical totals and records — a pending review can become visible without changing `X-Total-Count`. +5. After any follow-up push, redo the whole check against the new head; earlier reviews say nothing about the updated commit. + +Pagination on Forgejo 16.0.0: + +- **Reviews**: fetch `?limit=&page=N` for every page through `ceil(X-Total-Count / limit)`, taking the limit from `/settings/api` (`max_response_items`, 50 on code.j4k.dev). Don't stop at an underfilled page — Forgejo filters other users' pending reviews after database pagination, so a short page can precede later submitted reviews. The endpoint sends neither `Link` nor `X-HasMore`, so `fgj api --paginate` cannot prove completeness. Keep `PENDING` reviews in the sweep snapshot but don't treat their unpublished bodies or comments as feedback. A persistent gap between `X-Total-Count` and the deduplicated visible reviews is another user's unpublished draft — record it as a diagnostic, not a handoff blocker. +- **Issue comments**: one fetch returns everything — the endpoint ignores `page` and `limit`. Require the distinct returned IDs to match its `X-Total-Count`. + +`gh-feedback summary --json` aggregates issue comments, the inline review comments it reaches, and their reactions and responses — useful, but not proof of completeness: it omits review summary bodies and its Forgejo pager stops on an underfilled review page. Use it alongside the sweep, not instead of it. + +When processing or handing off a PR — not during a read-only audit — address every actionable finding and acknowledge the response on the PR before reporting completion. + +**Resolving conversations depends on the instance.** The j4k Forgejo fork adds a REST conversation-resolve API; stock Forgejo — upstream and codeberg.org, through 16.0.x — has none. Detect the capability from the `version` endpoint (`fgj --hostname "$apiHost" api version` — explicit host, or a probe run outside the target repo answers for `fgj`'s default host and reads that instance's capability instead; or `curl -fsS "$apiBase/version"` — `-f` so an HTTP error exits nonzero instead of handing the substring test an error body): the version string contains `-j4k` (live: `16.0.1-j4k.1+gitea-1.22.0`) on the fork, and `code.j4k.dev` qualifies — test for the substring, since build metadata (`+gitea-…`) trails the marker. The substring proves fork lineage, not the endpoint: an older `-j4k` server that predates the resolution route passes the test and `404`s on the first resolve — treat that like an unreadable version, skipping resolution for the pass and reporting it. A version without `-j4k` is stock Forgejo (e.g. codeberg.org's `16.0.0-dev-626-32363b81+gitea-1.22.0`); an unreadable version — `404`, network error, a body that isn't JSON with a `.version` string — blocks only resolution, not the feedback pass: process feedback normally, skip the resolve step, and report the failed probe rather than treating it as a stock verdict. This is the server's version — don't infer it from `fgj --version`, which reports the CLI build, a separate j4k fork that talks to stock servers just fine; that gate is effectively always true on this machine while the target instance may well be stock. Comment-minimize exists on neither build — Forgejo has no minimize concept at all. + +On a `-j4k` instance, `gh-feedback` v3.3.0+ owns native transitions for items it tracks; older builds use the feedback skill's qualified direct fallback. Raw-only workflows resolve with `fgj --hostname "$apiHost" pr review resolve -R "$slug"` and reopen with `… unresolve` (j4k `fgj` build `v0.5.0-j4k.4`+) — the explicit `-R` matters as much here as anywhere, since an omitted repo falls back to cwd detection. Any comment id in the thread works: the command lists the PR's reviews and their comments itself, walks to the thread's anchor, names the anchor it targeted, and reports the updated `resolver` (`--json` returns the updated anchor comment). Gate on the parent's subcommand listing per the j4k `fgj` build rule — on an older j4k build without the verb the call dies with cobra's `accepts 1 arg(s), received 3`, a usage error that means the verb is missing, not that you mistyped, and the fix is installing the newest build — not scripting a raw fallback; the REST path behind the verb goes deliberately unnamed here so it can't accrete one. Argument mistakes fail before any write with their own messages — `failed to list reviews` for a wrong PR number or slug, `not an inline review comment` for a review summary, issue comment, or foreign id (they aren't conversations) — and that second message also covers an inline id deleted since your sweep listed it, because the command re-lists every review comment before writing: re-run the sweep before reading it as your own mistyped id. A `404` from the resolve call itself means the server has no resolution API — stock Forgejo, or a `-j4k` server predating the route; the only deleted-id `404` is a comment vanishing in the instant between the command's own listing and its write. Token auth, same gate as other PR writes; success returns the updated anchor, whose `resolver` reflects real DB state and stays the original resolver on an idempotent re-resolve. + +**Verification still needs the conversation partition.** The resolve endpoint writes `resolver` to exactly the comment it is handed, and the UI reads a conversation's resolved state only from its anchor — that is why the command walks to the anchor before writing, and why the completeness sweep must read each anchor's `resolver` rather than any reply's. The API exposes no threading field, so derive conversations from the sweep's inline comments: a conversation is the code comments sharing a `path`, a side with its display line, and a `pull_request_review_id` — replies join the review they answer, while comments that different reviews leave at the same line are separate conversations, each resolved on its own. The side is whichever of `position` (new side) / `original_position` (old side) is nonzero — the unused side reports `0`, and old-side and new-side comments at the same number are distinct conversations; both at `0` is the stored line-`0` edge (the server keeps one signed line and reports it on a single side), which groups like any other value — same review, same path, line `0`, one conversation — and the display line is that number plus `extra_lines_count` — upstream Forgejo API since `v16.0.0`, where multi-line comments landed, not a fork field: Forgejo buckets a multi-line comment at the _end_ of its range, so a comment spanning 55–60 and a single-line comment at 60 from the same review are one conversation, and grouping by the raw `position` pair alone splits them and mis-picks the anchor. The anchor is the conversation's earliest comment (`created_at`, ties by lowest `id`). This partition is the one the API listings and the Conversations tab render; the sweep's `position` is the line as of the comment's own commit, while the diff view re-blames comments to the current head and can merge same-review buckets after lines move — don't expect it to mirror the derivation. `gh-feedback` derives this same native partition when reading or changing resolver state, but its feedback items still thread by reply markers: an item id is therefore not necessarily the anchor, and two findings one review left at the same display line are separate items sharing one conversation whose single resolve state — the anchor's, per the write-where-handed endpoint above, so a sibling root comment can carry a stray `resolver` from a direct write without changing what renders — speaks for both. Confirm the anchor's `resolver` is set after resolving — the per-review comment listings the sweep already fetches return it on every comment — and read it first to know the current state before unresolving. + +On a stock instance no resolve endpoint exists — the review `dismissals` endpoint dismisses a review's verdict, not a conversation, and the web "Resolve conversation" route authenticates by browser session cookie only, so a token POST 303-redirects to `/user/login` and resolves nothing. Track "done" by reaction there instead of pretending to resolve. + +# Rule: Generated Agent-File Drift Is Expected + +Automated tooling rewrites tracked agent instruction files (`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md`) whenever shared rules change, so these files may sit modified for long stretches. + +When they are the only modifications, treat the repository as clean — don't stash, revert, or delete the changes, and don't commit them on their own. Include them in the next substantive commit or pull request instead. + +# Rule: Local Third-Party Checkouts + +Before searching the web or relying on memory for an external library, check `~/Developer/third-party/` — it holds dozens of third-party repos cloned locally for reference. The source is faster and more authoritative than any secondary description. + +Run `ls ~/Developer/third-party/` to see what's available, then explore as with any local code. Don't clone new repos into this directory unsolicited — the user curates it. + +# Rule: Target Bash 3.2 + +Unless the user specifies otherwise, write shell scripts for bash 3.2 — the default `/bin/bash` on macOS. + +When using `printf`, do not pass a literal string starting with `-` as the format string. Use `printf '%s\n' "$value"` or `echo` so Bash 3.2 does not parse the marker as an option. + +Backslash line continuation inside a `for … in` word list across multiple lines is fragile in Bash 3.2 and can fail with "syntax error near unexpected token `done`". Collect the patterns in an array (`globs=(g1 g2 g3)`) and iterate with a nested loop, or put all patterns on a single line. + +# Rule: No Backwards Compatibility + +Prioritize the best shape of the current codebase over backwards compatibility. When something changes, change it completely — rename the function, delete the old flag, stop reading the old config path. Let the code reflect what it _is_, not what it _was_; version control remembers what was there. + +Remove every form of backwards-compatibility ballast: + +- Aliases and re-exports keeping old import paths or type names working +- Fallback reads from old config locations or env var names +- Renamed-but-kept flags, options, or CLI arguments +- Feature flags gating already-shipped changes +- Underscore-prefixed "unused" variables kept to silence lint +- `// removed: …` or `// TODO: delete in v3` comments marking absent code +- Deprecated types re-exported from their old module +- Database migrations committed before the project has real users — reset the dev DB and iterate + +Compatibility code accumulates as dead weight: it obscures intent, inflates surface area, and forces every future reader to reason about states that no longer exist. + +## Database migrations + +Committed migrations are permanent — every schema tweak becomes a file that travels with the project forever, even if the only "old" schema lived for an afternoon in a dev DB. Before a deployment holds data you can't discard, reset the dev DB and iterate on the schema directly; commit only the current state. A squashed clean initial schema is far cheaper to reason about than a chain of pre-release churn. + +Once real users have real data, the mode flips: every schema change becomes a migration, no exceptions. The cutover is "first deployment with persistent real data," not "first commit" or "first merged PR." The same reasoning extends to any on-disk or over-the-wire format you cannot discard — serialized caches, stored session blobs, published API shapes — iterate freely before the first real reader exists, then lock it down. + +# Rule: Access OrbStack Machines with `orb`, Not SSH + +To run a command inside an OrbStack Linux machine, use `orb -m -u ` (wrap in `bash -lc '…'` when you need a login shell, PATH, or pipes). The standing dev VM is `debian` with user `j4k`: + +```bash +orb -m debian -u j4k bash -lc 'node --version' +``` + +Don't reach for `ssh @.orb.local` — it authenticates by public key and fails with `Permission denied (publickey)` unless that user already has your key in `authorized_keys`. `orb` reuses OrbStack's host identity mapping, so it needs no key and works for any existing user on the machine. + +# Rule: Package Manager Execution + +How different package manager commands resolve binaries: + +| Command | Behavior | +| ----------------- | ----------------------------------------------------------------------- | +| `pnpm exec foo` | Runs from `./node_modules/.bin`; falls back to system PATH | +| `pnpx foo` | Always fetches from registry (uses dlx cache); ignores local installs | +| `npx foo` | Checks local `node_modules/.bin` → global → downloads from registry | +| `npx foo@version` | Resolves version, uses local if exact match exists, otherwise downloads | + +`pnpx` is an alias for `pnpm dlx`. + +# Rule: Prefer OrbStack Locally + +Use OrbStack as the local container and Linux VM runtime on macOS — not Docker Desktop, Colima, or a Podman machine. The `docker` and `docker compose` CLIs work unchanged; `orbctl` (aliased `orb`) creates and manages full Linux VMs. + +OrbStack has faster cold starts, lower idle CPU and memory, native macOS file sharing without bind-mount workarounds, and a single tool for both containers and VMs. Assume any container or VM workflow on this host runs through OrbStack. + +For ad hoc Linux VM testing, see the `orbstack-ad-hoc-vm` skill. + +# Rule: Prefer TypeScript Over Python + +When writing new code and the user states no language requirement, default to TypeScript. This yields to explicit user input: write Python when the user asks, when the task lives in a Python codebase, or when the ecosystem forces it (data science, ML, a Python-only library). + +# Rule: Project Skill Symlinks + +Keep the real skill in `.agents/skills//` and treat `.claude/skills/` as a symlink to it, not a second copy. Create the Claude entry with `ln -s ../../.agents/skills/ .claude/skills/` so both locations point at the same source of truth. + +# Rule: Runtime Tool Discovery + +When a workflow references a custom CLI — a local script or anything you may not already know — run ` --help` before first use. The help output is the authoritative source for subcommands, flags, and usage; rules only name the tool and rely on `--help` to teach you the rest at runtime. + +Compose these tools with pipes like any Unix CLI, and prefer machine-readable formats (`--porcelain`, `--json`) over parsing human-readable output. + +# Rule: Repository Scripts + +`scripts/` is the default home for the repository's operational tooling — automation, helpers, and one-offs that maintain the repo but aren't part of what it ships. + +**Look there first.** If `scripts/` exists, `ls scripts/` and `jq '.scripts' package.json` before writing anything new — extend what's there rather than forking it. + +**Put new scripts there by default.** Anything worth committing — release helpers, submodule updates, git hooks, recurring chores — goes in `scripts/`, wired through `package.json` so callers invoke it by name rather than remembering the path. + +# Rule: Set an Explicit Timeout for Long CI Waits + +Waiting on CI to finish — `gh run watch`, `fgj actions run watch`, or a poll loop over `gh run list --commit ` — routinely outlasts an agent shell tool's default command timeout (Claude Code's Bash tool defaults to 120s, raisable to 600s). Pass an explicit longer timeout (e.g. 420000 ms) to the command invocation, or the wait is killed mid-run and reports a false failure. + +# Rule: Sub-Agent Delegation + +Spawn sub-agents liberally. A sub-agent encapsulates a chunk of work behind a simple interface: briefed on the task, it gathers its own context, works autonomously, and hands back only the result — trust the process and engage with the outcome. The main agent's job is to frame tasks, dispatch, and synthesize results. + +**Delegate, move on, verify.** A task of many simple steps is prime delegation material — deploying an Ansible playbook and combing its verbose logs where nearly every task just reports ok, provisioning a throwaway OrbStack VM with Node, Docker, and Postgres installed, clicking through a multi-page web flow filling fields and pressing buttons, watching a CI run, applying a bulk mechanical edit. Hand it to a background sub-agent, move on to other work while it runs, and when it finishes, spawn a fresh sub-agent to confirm the work was done correctly — and for critical work, several adversarial reviewers, each attacking the result through a different lens. + +**Skills delegate too.** Instead of invoking a skill yourself, consider handing it to a sub-agent: name the skill and the inputs, and the sub-agent loads it, follows the workflow, and returns the result — the skill's full instruction set never enters the main context. `agent-browser` is the perfect shape for this: "log in to the site and check that such-and-such feature works" is a one-line instruction with a one-line answer, and everything in between — dozens of tool calls, failed selectors, retries — stays encapsulated in the sub-agent. Whether that fits is a per-skill call. + +# Rule: TSV Parsing + +`awk` splits on any whitespace by default, silently breaking on TSV values containing spaces. For tab-separated output (often `--porcelain` flags), set the delimiter explicitly: + +```bash +# BAD: prints "name" instead of "name with spaces" +printf 'id\tname with spaces\tstatus\n' | awk '{ print $2 }' + +# GOOD — pick one: +awk -F'\t' '{ print $2 }' +cut -f2 # cut defaults to tab +while IFS=$'\t' read -r a b c; do …; done +``` + +Empty fields are a second trap for the `read` form only: tab is IFS _whitespace_, so runs of tabs collapse into one delimiter and an empty middle field shifts every value after it — `awk -F'\t'` and `cut -f` are immune. When a field can be empty, prefer `awk`/`cut`, keep nullable fields last, or in zsh double the tab (`IFS=$'\t\t'`), which the manual defines as demoting it to a hard delimiter that preserves empty fields; bash has no doubled form — it silently ignores the doubling and shifts the fields anyway, so a bash-run copy of the zsh idiom reinstates the exact bug it exists to prevent, with no diagnostic. + +Not every `--porcelain` is TSV — `git worktree list --porcelain` is space-separated per line. Sample output before picking a delimiter. + +# Rule: Reach for Unix-Native Primitives Before Inventing Abstractions + +Use the OS as the first control plane. Before proposing a registry, supervisor, scheduler, logger, IPC layer, config store, or discovery protocol, check whether argv, environment variables, inherited file descriptors, filesystem paths, Unix-domain sockets, ports, signals, stdout/stderr, cron, systemd, or XDG paths already solve it. + +Two primitives cover almost everything: + +- **Handoff at fork/exec** — parent passes addresses to children via args, env vars, or inherited FDs (`SSH_AUTH_SOCK`, systemd socket activation). +- **Well-known names in a shared namespace** — filesystem paths and TCP/UDP ports (`/var/run/docker.sock`, port 22). The filesystem is the service directory. + +Common problems map directly: process discovery → socket at a conventional path or env var; IPC → Unix-domain socket, named pipe, or signal; supervision → systemd or another init; scheduling → cron or systemd timer; logging → stdout/stderr; config → XDG config dir. + +For Node apps needing a config, data, cache, log, or temp directory, default to [`env-paths`](https://github.com/sindresorhus/env-paths). It returns the right location per platform — XDG on Linux, `~/Library/...` on macOS, `%APPDATA%` on Windows — so you don't hand-roll `process.platform` branches that drift. Pass a namespace (`envPaths('my-app')`) and use the returned `config`, `data`, `cache`, `log`, `temp` paths directly. + +**Don't build a second control plane.** Reject the native primitive only when you can name the concrete property it cannot provide: distributed discovery across hosts, authorization the OS namespace cannot enforce, schema evolution for a long-lived wire format, multiplexing many streams over one transport, binary streaming with backpressure, or cross-platform targets where no equivalent primitive exists everywhere. + +Otherwise the native primitive wins. Designs that don't compose with pipes, signals, and conventional file locations pay a tax forever. + +# Rule: Use Native TypeScript Execution + +Use Node 24+ and run `.ts` files directly with `node script.ts`. Node strips types at runtime — no `tsx`, no `ts-node`, no `tsc` build step. + +Default to `.ts` over `.mjs` for new scripts and to `node` over `tsx` in `package.json`. diff --git a/archive/global-targets/opencode-AGENTS.md b/archive/global-targets/opencode-AGENTS.md new file mode 100644 index 0000000..3453a85 --- /dev/null +++ b/archive/global-targets/opencode-AGENTS.md @@ -0,0 +1,377 @@ +# Rule: 1Password Commit Signing + +This machine signs git commits via 1Password. Any signing error during `git commit` — 1Password socket errors, "failed to sign the data", "fatal: failed to write commit object" — usually means 1Password is locked. Ask the user to unlock 1Password, then retry the commit. + +# Rule: `AGENTS.md` Is Generated — Edit the Source + +`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md` and the like are generated by the `sync-rules` CLI. The next `sync-rules` run overwrites direct edits. + +To change what an agent reads, edit one of two sources: + +- **Rule content** in `~/Developer/j4k/setup-atlas/rules//*.md` — reword an existing file or add a new one under the appropriate category. +- **Which rules apply to which repo** in `/Users/jercik/Library/Preferences/sync-rules/config.json` — add or remove a glob (e.g., `"nodejs/*.md"`) under a `projects[].path` entry, or under `global` / `globalOverrides` for cross-repo defaults. + +Run `sync-rules` after either edit. + +# Rule: Automatic Repository Alignment Scope + +When the user asks to align or audit "all my repositories", that means the repository checkouts directly under `~/Developer/jercik/`, `~/Developer/j4k/`, and `~/Developer/j4k-oss/` — nothing outside those three directories. + +# Rule: Canonical Repository Checkout Layout + +Place every owned repository under `~/Developer//`, using the remote owner and repository slugs rather than a locally invented prefix: + +- GitHub `Jercik/example` → `~/Developer/jercik/example` +- GitHub `validationcloud/example` → `~/Developer/validationcloud/example` +- Forgejo `j4k/example` → `~/Developer/j4k/example` +- Forgejo `j4k-oss/example` → `~/Developer/j4k-oss/example` + +Use the remote repository name verbatim. A historical local prefix is not part of the name: Forgejo `j4k/align` belongs at `~/Developer/j4k/align`, never `~/Developer/j4k-align`. + +Git worktrees live in the same owner directory as the main checkout, with the branch name appended to the directory name as `-`: a worktree for branch `foo` of `~/Developer/j4k/align` goes at `~/Developer/j4k/align-foo`. Create worktrees with `worktree-add ` from inside the repository — it places the new checkout at that canonical path automatically, copies useful local files, and installs dependencies. + +Before cloning, resolve the forge and owner/repository slug, create the owner directory, and pass the canonical destination explicitly. The account-aware `gh` shim selects the ValidationCloud account and SSH key for `validationcloud/*` and `lukasz-jercinski-vc/*` targets, and the personal account otherwise. Forgejo clones use the canonical tailnet SSH transport and an explicit destination. `code.tail.j4k.dev` is reachable only while the machine is connected to the Tailscale network — a clone or fetch that hangs or can't connect usually means the tailnet is down, not that the key or remote is wrong; check the Tailscale connection before debugging SSH. + +Third-party source remains under `~/Developer/third-party/` and is never reorganized by owner. Do not infer an owner for a Git root with no remote or for a plain local directory; leave it in place until the user classifies it. + +# Rule: You Share This Workspace + +Other agents and the user may have uncommitted WIP in the working tree, and new changes can appear mid-session. Don't assume unexpected state came from your edits, and don't stash, overwrite, or commit work you didn't make — even stash-then-pop can confuse another agent whose tree state shifts underfoot. + +If you need an isolated tree, ask the user about creating a git worktree and move your changes there. + +# Rule: Conventional Commits + +Write every git commit message and pull request title in Conventional Commits format (`type: subject`). + +Before authoring a PR title and body, load the `pr-writing-style` skill if it is installed — it owns the prose: title wording, body shape, what gets cut. + +# Rule: Create PRs on the Repo's Forge (No Shell Expansion) + +Detect the forge first — [the forge detection rule](./forge-provider-detection.md) owns the how — and use the matching PR tool: `gh` for GitHub, `fgj` for Forgejo. Don't reach for `gh` reflexively; it can't open a PR on Forgejo. + +The shared trap is shell expansion of multi-line Markdown bodies. A double-quoted body string lets the shell expand backticks and `$...` before the CLI sees it, mangling code blocks and variable references. Pass the body so the shell never scans it. + +**GitHub (`gh`)** — `--body-file` with a single-quoted heredoc (`'EOF'` disables all expansion): + +```bash +gh pr create --title "docs: clarify example" --body-file - <<'EOF' +Clarifies that the `..${sep}` check doesn't block `..foo/bar.txt`. +EOF +``` + +**Forge (`fgj`)** — `fgj pr create` takes only `-b` (no `--body-file`), so write the body with a single-quoted heredoc, then pass it by command substitution. The file's bytes become the argument verbatim; the shell does not re-scan them for expansion: + +```bash +forge=$(mktemp) +npx -y repoq@latest forge --json > "$forge" +apiHost=$(jq -r '.apiHost' "$forge") # e.g. code.j4k.dev, codeberg.org +slug=$(jq -r '.slug' "$forge") +body=$(mktemp) +cat > "$body" <<'EOF' +Clarifies that the `..${sep}` check doesn't block `..foo/bar.txt`. +EOF +fgj --hostname "$apiHost" pr create -R "$slug" \ + --title "docs: clarify example" --base main --head my-branch \ + -b "$(cat "$body")" +rm -f "$body" "$forge" +``` + +When running from the target repo with the j4k build configured, `fgj` can auto-detect the repo and API host, including `git_hosts` aliases for SSH-only transport hosts. For scripts, cross-repo work, or commands launched outside the target repo, still pass `--hostname "$apiHost"` and `-R "$slug"` from `repoq forge --json`; explicit flags remove cwd and remote-alias ambiguity. `fgj pr edit -F ` accepts a body file if you'd rather create then edit. + +Verify with the matching CLI: `gh pr view --json title,body,url` or `fgj --hostname "$apiHost" pr view -R "$slug" --json`. + +# Rule: Doc-Comments and Example Snippets Are Documentation + +Audit JSDoc/docstrings and example code in source with the same rigor as markdown. Example calls must type-check against the current signature; a required `T | undefined` field omitted from an example is drift even though it "looks" fine. + +# Rule: Encode Cross-Repo Config Learnings in j4k-align + +`j4k-align` (in `~/Developer/j4k/align/`) audits and aligns repo settings on either forge, automation inputs, workflows, rulesets (GitHub) / branch protection (Forgejo), and managed files across every repo. Its checks and templates are the source of truth for cross-repo configuration — on-disk state drifts; the audit defines what "correct" means. + +j4k-align detects the forge per repo from its git origin and audits and aligns config across both GitHub (via `gh`) and the self-hosted Forgejo instance (`code.j4k.dev`, via `fgj`) end to end — there is no Forge gap to work around. + +When you hit a config issue in one repo — a misbehaving lint rule, a missing tsconfig field, a workflow that breaks on a new dependency — ask: **"Will this come up again in another repo?"** If yes, the fix belongs in `j4k-align`. Patching one repo lets the same trap resurface elsewhere; encoding it in the audit catches it everywhere. + +## Traits, triggers, and the extension loop + +Repos are classified by **traits** — labels like `pnpm-package`, `react`, `nextjs`, `external-references`, `private-oci-publish` defined in `src/alignment/schemas.ts`. Each trait is assigned by a **trigger** — a signal from the repo (file presence, `package.json` dependency, `.gitmodules` content, registry config). Traits gate checks and template selection. + +Touchpoints when extending the audit: + +- Trigger: `src/traits/signals.ts` +- Trait assignment: `src/traits/build-traits.ts` (enum in `src/alignment/schemas.ts`) +- Trait-gated check: `src/cli/verify/check-*.ts`, wired in `src/cli/verify/run-checks.ts` +- Fix implementation: `src/checks/*.ts`, wired in `src/cli/fix/local-project-configs/fix-local-tool-configs.ts` +- Templates: `templates/`, with `TraitSelection` guards in `src/resolve-template-files.selections.ts` + +**Fix the signal, not the symptom.** If a repo should have a trait but doesn't — or has one it shouldn't — the trigger is wrong. Repair `src/traits/signals.ts` rather than hardcoding the trait downstream. Verify with `j4k-align --repo owner/name`, then `--fix` to confirm remediation. + +## Forbid the trap, not just the current case + +When you hit a known footgun — a setting that silently breaks types, a flag that disables a guarantee, a path pattern that traps on edge cases — add a check that rejects that value across every applicable trait, not one that only repairs the current repo. Forbid the specific bad shape, name the check clearly, and record the failure mode it prevents inside the check so the reasoning travels with the code. + +## Scope + +j4k-align governs cross-repo configuration: build, lint, format, tsconfig, workflows, rulesets (GitHub) / branch protection (Forgejo), repo settings on either forge, automation secrets and variables, and managed files (git hooks, `release.config.mjs`, etc.). Project-specific business code, product schema, and feature behavior stay in their repos. + +## Anti-patterns + +- `eslint-disable`, `// @ts-ignore`, or `.gitignore` entries that silence a warning other repos will hit identically. +- Hand-editing `.github/workflows/*.yml` or `.forgejo/workflows/*.yml` when the source-of-truth template lives in `templates/`. +- Diverging a per-repo `tsconfig.json` to dodge a check instead of fixing the check. +- Hardcoding a trait or skipping a check when the real bug is in the trigger. + +This is a specialized application of _Fix the Foundation First_ — the foundation here is the trait/trigger/check/fix pipeline. + +# Rule: `eslint-config-axkit` Is Deprecated + +Superseded by oxlint with `@j4k/oxlint-config`; it cannot run under the fleet's `typescript@^7` pin, which breaks `typescript-eslint`. Never add it to a project — when touching a repo that still lints through it, migrate to oxlint instead. + +# Rule: Use the j4k Custom Build of `fgj` + +The j4k fork at [`codeberg.org/jercik/fgj`](https://codeberg.org/jercik/fgj) ships features not yet in upstream `romaintb/fgj` — `fgj pr review` with inline comments, `fgj pr review resolve`/`unresolve` for conversation resolution (against the j4k Forgejo fork's resolution API), `fgj pr checks` for a PR's combined CI status, the `fgj pr list --base`/`--head` server-side filters, org-scoped `fgj actions` secrets/variables (with pipeable secret input), and `git_hosts` aliases for remotes whose SSH host differs from the Forgejo API host. Features leave this list as they merge: the generic `fgj api` passthrough and `fgj repo view --json` landed upstream in `v0.5.0` (2026-07). Other rules here lean on these commands, so prefer this build over the stock Homebrew one. + +**Check the build, not the version number.** The `-j4k.N` suffix on `fgj --version` is the durable tell; a bare upstream version (e.g. `0.4.0`) means stock. Gate on the suffix, which survives version bumps: + +```bash +fgj --version | grep -q j4k || echo "stock fgj — install the j4k build" +``` + +The suffix only proves the fork lineage, not any one feature — features accrete across `-j4k.N` releases, so an older fork build passes the gate while missing a newer verb. When a workflow depends on a specific command, gate on the parent's subcommand listing. An exit-code probe (`fgj pr review resolve --help`) false-passes on the build it exists to catch: cobra reads the unknown word as a positional argument and answers `--help` with the parent's help, exit `0`. The listing discriminates — scoped to the `Available Commands:` block, so an indented prose or example line that happens to start with a verb name can't satisfy it — and checks every verb the workflow needs, since the accretion argument above applies verb by verb: + +```bash +help=$(fgj pr review --help) \ + || { echo "fgj pr review --help failed — is fgj installed?" >&2; exit 1; } +verbs=$(awk '/^Available Commands:/{f=1;next} /^[^[:space:]]/{f=0} f' <<<"$help") +for v in resolve unresolve; do + grep -qE "^[[:space:]]+$v([[:space:]]|$)" <<<"$verbs" \ + || { echo "fgj build has no $v verb — install the newest v*-j4k.* release" >&2; exit 1; } +done +``` + +The `--help` failure branch is the missing-or-broken-binary case — cobra answers `--help` with exit `0` even on a build with no `pr review` at all (the parent-fallback above), so a nonzero exit means the command never ran. Every other shape falls through to the per-verb check, whose message names the right remedy for all of them: the parent `pr` help of a stock or pre-`pr review` fork build, and even a `pr review` leaf command whose help prints no `Available Commands:` block at all — released j4k builds always print it (`pr review` shipped with `list` and `comments` already registered), but the probe no longer leans on that history to route the failure. The `exit 1` makes the snippet a hard gate for scripts; a workflow that can degrade instead — like the feedback-processing skill, which skips only the resolve step on a failed probe — runs the same probe and branches on its exit status rather than dying. + +Install the newest `v*-j4k.*` tag from the [releases page](https://codeberg.org/jercik/fgj/releases) — `scripts/install.sh` is checksum-verified for CI and local machines; in a Dockerfile, pull the `linux_{amd64,arm64}.tar.gz` release asset directly. Binaries are static and CGO-free, so they run anywhere (alpine, distroless, scratch). + +For Forge repos whose `origin` uses a transport-only host such as `code.tail.j4k.dev`, configure the API host with a Git alias instead of passing the SSH host as `--hostname`: + +```bash +fgj auth login --hostname code.j4k.dev --git-host code.tail.j4k.dev +``` + +Features are upstreamed one small PR at a time. Until a feature lands upstream and ships in a stock release, the j4k build is the source of truth. + +# Rule: Fix the Foundation First + +When you hit an issue likely to recur, stop and solve the underlying problem rather than working around it. The upfront investment pays for itself every time the issue would have resurfaced. + +For example: if you don't know how an API works and will need it repeatedly, don't guess at endpoints — build a tool that fetches and displays its docs. If a manual step keeps recurring, automate it. If knowledge is missing, capture it in a script, command, or doc so it's available next time. + +The key question: _"Will this come up again?"_ If yes, fix the root cause now. + +# Rule: Detect the Forge — GitHub (`gh`) vs Forgejo (`fgj`) + +Repos are mid-migration across two forges, so **never assume `gh`** — it only speaks GitHub's API. Detect the provider from `origin` before any forge operation (PRs, issues, CI/checks, releases, repo metadata, branch protection) and use the matching CLI. + +- **GitHub** — `github.com`, CLI `gh`, names untouched (`Jercik/j4k-cluster`). +- **The Forge** — self-hosted Forgejo at `code.j4k.dev`, CLI [`fgj`](https://codeberg.org/romaintb/fgj). Repos live under `j4k/` and drop the `j4k-` affix (`j4k-cluster` → `j4k/cluster`; an already-unprefixed repo like `setup-atlas` keeps its name). + +## Detection + +Run `npx -y repoq@latest forge --json`. The explicit tag prevents `npx` from reusing a stale cached release. It normalizes every URL shape (scp-like, `ssh://`, `https://`) and handles the `j4k-` affix that a hand-rolled `sed` gets wrong, returning `provider` (`github`/`forgejo`/`unknown`), `cli`, `slug`, `hostnameFlag`, `apiHost`, `apiBase`, plus `host`/`webHost`/`sshHost`/`owner`/`repo`. Reach for `apiHost` when passing a host to `fgj` — `hostnameFlag` is a two-word string that zsh, which does not word-split expansions, hands `fgj` as a single argument. Treat `unknown` as a hard stop. + +## Driving `fgj` + +`fgj` mirrors `gh`'s verbs (`pr`, `issue`, `release`, `repo`, `label`, `milestone`, with `--json` on the read verbs). For a PR's combined CI status, use `fgj pr checks ` (add `--json` for machine-readable output; exit codes match `gh pr checks` — `0` success/warning/skipped/no-checks, `1` failure/error, `8` pending). CI run history lives under `fgj actions run list|view|watch`; for step logs use `fgj actions run view --log` (also `--log-failed`, `-j `) — don't decompress the on-disk `actions_log/*.zst` chunks. + +When running inside the target repo, the j4k build of `fgj` can auto-detect the repo and API host. If `origin` uses a transport-only SSH host such as `ssh://git@code.tail.j4k.dev:2222/…`, configure that hostname as a `git_hosts` alias under the real API host (`code.j4k.dev`) and do not pass the SSH host as `--hostname`. + +For scripts, cross-repo commands, or work launched outside the target repo, use `repoq forge --json` and pass the returned host and slug explicitly: `fgj --hostname "$apiHost" -R "$slug" ...`. Explicit flags are still the most deterministic shape when cwd or remote config might not describe the target repo. The leading position is convention, not requirement — `--hostname` is a root persistent flag that cobra parses before or after the subcommand, so a trailing literal `--hostname ` (the `fgj auth` examples) is equally correct; the hazard the convention guards is the two-word `$hostnameFlag` expansion, which breaks at any position. One nuance, not an exception: the `auth` verbs declare a local `--hostname` that shadows the root flag, but cobra hands a leading flag to that local flag too, so both positions keep working — the difference is only that the shadowed root value never reaches the config fallback, and `auth login` has no fallback at all: pass its `--hostname` explicitly (either position) or it prompts interactively. + +## Beyond `fgj`'s verbs: `fgj api` + +Some operations have no dedicated verb. Reach for the generic `fgj api ` passthrough — it reuses the configured auth and resolved API host. The passthrough started in [the j4k custom build of `fgj`](./fgj-custom-build.md), which the other `fgj` rules already assume, and landed upstream in `v0.5.0` (2026-07); on an older stock `fgj` without it, fall back to `curl "$apiBase/"` with a token from `fgj auth token --hostname "$apiHost"` — capture it first and require it non-empty (`token=$(fgj auth token --hostname "$apiHost")`; an empty substitution would send an unauthenticated request whose `401` reads as an instance fault instead of a missing token), then pass it as a `curl` config on stdin — `printf 'header = "Authorization: token %s"\n' "$token" | curl -fsS --config - "$apiBase/"` — not as an `-H` argv header any process on the machine could read from the process table. + +- **Branch protection** (the Forge's replacement for GitHub "rulesets"): `/repos/{owner}/{repo}/branch_protections[/{name}]`. +- **A PR's combined CI status**: use the dedicated `fgj pr checks ` command instead — not the raw passthrough (add `--json` for machine-readable output; exit codes match `gh pr checks` — `0` success/warning/skipped/no-checks, `1` failure/error, `8` pending). +- **Combined commit status for an arbitrary commit not tied to a PR**: `/repos/{owner}/{repo}/commits/{sha}/status`. +- **Repo metadata as JSON**: `/repos/{owner}/{repo}` — or just `fgj repo view --json` (upstream since `v0.5.0`, alongside the passthrough). + +## Inspecting all Forgejo PR feedback + +Forgejo splits PR discussion across three API surfaces: issue comments at `/repos/{owner}/{repo}/issues/{number}/comments`, review summaries at `/repos/{owner}/{repo}/pulls/{number}/reviews`, and inline comments under each review at `/repos/{owner}/{repo}/pulls/{number}/reviews/{review-id}/comments`. Reading one surface misses the others: a review's summary body often carries only a count ("Found 2 medium issues") while the findings live in its inline comments, and responses land as issue comments. Web URLs anchor inline comments as `#issuecomment-`, but the API exposes them only under `/pulls/{number}/reviews/{review-id}/comments`, never in `/issues/{number}/comments`. + +Before handing off a PR, prove the review cycle is complete for the current head: + +1. Wait for the `PR Review` action run whose `commit_sha` equals `git rev-parse HEAD` (the run appears asynchronously — retry an empty result), then wait until every job in that run is terminal. A run-level failure or a terminal `fgj pr checks` does not mean all reviewer jobs finished. +2. Read the expected reviewer set from that run's jobs endpoint, not from workflow files in the checkout — `pull_request_target` executes the base branch's workflow. +3. Require every expected non-skipped reviewer job to have a submitted review whose `commit_id` is that head, and `fgj pr checks ` to report the same head in `.sha`. A reviewer job that exits without publishing a verdict blocks handoff — retry it. +4. Sweep all three surfaces (pagination rules below) and repeat until two consecutive sweeps return identical totals and records — a pending review can become visible without changing `X-Total-Count`. +5. After any follow-up push, redo the whole check against the new head; earlier reviews say nothing about the updated commit. + +Pagination on Forgejo 16.0.0: + +- **Reviews**: fetch `?limit=&page=N` for every page through `ceil(X-Total-Count / limit)`, taking the limit from `/settings/api` (`max_response_items`, 50 on code.j4k.dev). Don't stop at an underfilled page — Forgejo filters other users' pending reviews after database pagination, so a short page can precede later submitted reviews. The endpoint sends neither `Link` nor `X-HasMore`, so `fgj api --paginate` cannot prove completeness. Keep `PENDING` reviews in the sweep snapshot but don't treat their unpublished bodies or comments as feedback. A persistent gap between `X-Total-Count` and the deduplicated visible reviews is another user's unpublished draft — record it as a diagnostic, not a handoff blocker. +- **Issue comments**: one fetch returns everything — the endpoint ignores `page` and `limit`. Require the distinct returned IDs to match its `X-Total-Count`. + +`gh-feedback summary --json` aggregates issue comments, the inline review comments it reaches, and their reactions and responses — useful, but not proof of completeness: it omits review summary bodies and its Forgejo pager stops on an underfilled review page. Use it alongside the sweep, not instead of it. + +When processing or handing off a PR — not during a read-only audit — address every actionable finding and acknowledge the response on the PR before reporting completion. + +**Resolving conversations depends on the instance.** The j4k Forgejo fork adds a REST conversation-resolve API; stock Forgejo — upstream and codeberg.org, through 16.0.x — has none. Detect the capability from the `version` endpoint (`fgj --hostname "$apiHost" api version` — explicit host, or a probe run outside the target repo answers for `fgj`'s default host and reads that instance's capability instead; or `curl -fsS "$apiBase/version"` — `-f` so an HTTP error exits nonzero instead of handing the substring test an error body): the version string contains `-j4k` (live: `16.0.1-j4k.1+gitea-1.22.0`) on the fork, and `code.j4k.dev` qualifies — test for the substring, since build metadata (`+gitea-…`) trails the marker. The substring proves fork lineage, not the endpoint: an older `-j4k` server that predates the resolution route passes the test and `404`s on the first resolve — treat that like an unreadable version, skipping resolution for the pass and reporting it. A version without `-j4k` is stock Forgejo (e.g. codeberg.org's `16.0.0-dev-626-32363b81+gitea-1.22.0`); an unreadable version — `404`, network error, a body that isn't JSON with a `.version` string — blocks only resolution, not the feedback pass: process feedback normally, skip the resolve step, and report the failed probe rather than treating it as a stock verdict. This is the server's version — don't infer it from `fgj --version`, which reports the CLI build, a separate j4k fork that talks to stock servers just fine; that gate is effectively always true on this machine while the target instance may well be stock. Comment-minimize exists on neither build — Forgejo has no minimize concept at all. + +On a `-j4k` instance, `gh-feedback` v3.3.0+ owns native transitions for items it tracks; older builds use the feedback skill's qualified direct fallback. Raw-only workflows resolve with `fgj --hostname "$apiHost" pr review resolve -R "$slug"` and reopen with `… unresolve` (j4k `fgj` build `v0.5.0-j4k.4`+) — the explicit `-R` matters as much here as anywhere, since an omitted repo falls back to cwd detection. Any comment id in the thread works: the command lists the PR's reviews and their comments itself, walks to the thread's anchor, names the anchor it targeted, and reports the updated `resolver` (`--json` returns the updated anchor comment). Gate on the parent's subcommand listing per the j4k `fgj` build rule — on an older j4k build without the verb the call dies with cobra's `accepts 1 arg(s), received 3`, a usage error that means the verb is missing, not that you mistyped, and the fix is installing the newest build — not scripting a raw fallback; the REST path behind the verb goes deliberately unnamed here so it can't accrete one. Argument mistakes fail before any write with their own messages — `failed to list reviews` for a wrong PR number or slug, `not an inline review comment` for a review summary, issue comment, or foreign id (they aren't conversations) — and that second message also covers an inline id deleted since your sweep listed it, because the command re-lists every review comment before writing: re-run the sweep before reading it as your own mistyped id. A `404` from the resolve call itself means the server has no resolution API — stock Forgejo, or a `-j4k` server predating the route; the only deleted-id `404` is a comment vanishing in the instant between the command's own listing and its write. Token auth, same gate as other PR writes; success returns the updated anchor, whose `resolver` reflects real DB state and stays the original resolver on an idempotent re-resolve. + +**Verification still needs the conversation partition.** The resolve endpoint writes `resolver` to exactly the comment it is handed, and the UI reads a conversation's resolved state only from its anchor — that is why the command walks to the anchor before writing, and why the completeness sweep must read each anchor's `resolver` rather than any reply's. The API exposes no threading field, so derive conversations from the sweep's inline comments: a conversation is the code comments sharing a `path`, a side with its display line, and a `pull_request_review_id` — replies join the review they answer, while comments that different reviews leave at the same line are separate conversations, each resolved on its own. The side is whichever of `position` (new side) / `original_position` (old side) is nonzero — the unused side reports `0`, and old-side and new-side comments at the same number are distinct conversations; both at `0` is the stored line-`0` edge (the server keeps one signed line and reports it on a single side), which groups like any other value — same review, same path, line `0`, one conversation — and the display line is that number plus `extra_lines_count` — upstream Forgejo API since `v16.0.0`, where multi-line comments landed, not a fork field: Forgejo buckets a multi-line comment at the _end_ of its range, so a comment spanning 55–60 and a single-line comment at 60 from the same review are one conversation, and grouping by the raw `position` pair alone splits them and mis-picks the anchor. The anchor is the conversation's earliest comment (`created_at`, ties by lowest `id`). This partition is the one the API listings and the Conversations tab render; the sweep's `position` is the line as of the comment's own commit, while the diff view re-blames comments to the current head and can merge same-review buckets after lines move — don't expect it to mirror the derivation. `gh-feedback` derives this same native partition when reading or changing resolver state, but its feedback items still thread by reply markers: an item id is therefore not necessarily the anchor, and two findings one review left at the same display line are separate items sharing one conversation whose single resolve state — the anchor's, per the write-where-handed endpoint above, so a sibling root comment can carry a stray `resolver` from a direct write without changing what renders — speaks for both. Confirm the anchor's `resolver` is set after resolving — the per-review comment listings the sweep already fetches return it on every comment — and read it first to know the current state before unresolving. + +On a stock instance no resolve endpoint exists — the review `dismissals` endpoint dismisses a review's verdict, not a conversation, and the web "Resolve conversation" route authenticates by browser session cookie only, so a token POST 303-redirects to `/user/login` and resolves nothing. Track "done" by reaction there instead of pretending to resolve. + +# Rule: Generated Agent-File Drift Is Expected + +Automated tooling rewrites tracked agent instruction files (`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md`) whenever shared rules change, so these files may sit modified for long stretches. + +When they are the only modifications, treat the repository as clean — don't stash, revert, or delete the changes, and don't commit them on their own. Include them in the next substantive commit or pull request instead. + +# Rule: Local Third-Party Checkouts + +Before searching the web or relying on memory for an external library, check `~/Developer/third-party/` — it holds dozens of third-party repos cloned locally for reference. The source is faster and more authoritative than any secondary description. + +Run `ls ~/Developer/third-party/` to see what's available, then explore as with any local code. Don't clone new repos into this directory unsolicited — the user curates it. + +# Rule: Target Bash 3.2 + +Unless the user specifies otherwise, write shell scripts for bash 3.2 — the default `/bin/bash` on macOS. + +When using `printf`, do not pass a literal string starting with `-` as the format string. Use `printf '%s\n' "$value"` or `echo` so Bash 3.2 does not parse the marker as an option. + +Backslash line continuation inside a `for … in` word list across multiple lines is fragile in Bash 3.2 and can fail with "syntax error near unexpected token `done`". Collect the patterns in an array (`globs=(g1 g2 g3)`) and iterate with a nested loop, or put all patterns on a single line. + +# Rule: No Backwards Compatibility + +Prioritize the best shape of the current codebase over backwards compatibility. When something changes, change it completely — rename the function, delete the old flag, stop reading the old config path. Let the code reflect what it _is_, not what it _was_; version control remembers what was there. + +Remove every form of backwards-compatibility ballast: + +- Aliases and re-exports keeping old import paths or type names working +- Fallback reads from old config locations or env var names +- Renamed-but-kept flags, options, or CLI arguments +- Feature flags gating already-shipped changes +- Underscore-prefixed "unused" variables kept to silence lint +- `// removed: …` or `// TODO: delete in v3` comments marking absent code +- Deprecated types re-exported from their old module +- Database migrations committed before the project has real users — reset the dev DB and iterate + +Compatibility code accumulates as dead weight: it obscures intent, inflates surface area, and forces every future reader to reason about states that no longer exist. + +## Database migrations + +Committed migrations are permanent — every schema tweak becomes a file that travels with the project forever, even if the only "old" schema lived for an afternoon in a dev DB. Before a deployment holds data you can't discard, reset the dev DB and iterate on the schema directly; commit only the current state. A squashed clean initial schema is far cheaper to reason about than a chain of pre-release churn. + +Once real users have real data, the mode flips: every schema change becomes a migration, no exceptions. The cutover is "first deployment with persistent real data," not "first commit" or "first merged PR." The same reasoning extends to any on-disk or over-the-wire format you cannot discard — serialized caches, stored session blobs, published API shapes — iterate freely before the first real reader exists, then lock it down. + +# Rule: Access OrbStack Machines with `orb`, Not SSH + +To run a command inside an OrbStack Linux machine, use `orb -m -u ` (wrap in `bash -lc '…'` when you need a login shell, PATH, or pipes). The standing dev VM is `debian` with user `j4k`: + +```bash +orb -m debian -u j4k bash -lc 'node --version' +``` + +Don't reach for `ssh @.orb.local` — it authenticates by public key and fails with `Permission denied (publickey)` unless that user already has your key in `authorized_keys`. `orb` reuses OrbStack's host identity mapping, so it needs no key and works for any existing user on the machine. + +# Rule: Package Manager Execution + +How different package manager commands resolve binaries: + +| Command | Behavior | +| ----------------- | ----------------------------------------------------------------------- | +| `pnpm exec foo` | Runs from `./node_modules/.bin`; falls back to system PATH | +| `pnpx foo` | Always fetches from registry (uses dlx cache); ignores local installs | +| `npx foo` | Checks local `node_modules/.bin` → global → downloads from registry | +| `npx foo@version` | Resolves version, uses local if exact match exists, otherwise downloads | + +`pnpx` is an alias for `pnpm dlx`. + +# Rule: Prefer OrbStack Locally + +Use OrbStack as the local container and Linux VM runtime on macOS — not Docker Desktop, Colima, or a Podman machine. The `docker` and `docker compose` CLIs work unchanged; `orbctl` (aliased `orb`) creates and manages full Linux VMs. + +OrbStack has faster cold starts, lower idle CPU and memory, native macOS file sharing without bind-mount workarounds, and a single tool for both containers and VMs. Assume any container or VM workflow on this host runs through OrbStack. + +For ad hoc Linux VM testing, see the `orbstack-ad-hoc-vm` skill. + +# Rule: Prefer TypeScript Over Python + +When writing new code and the user states no language requirement, default to TypeScript. This yields to explicit user input: write Python when the user asks, when the task lives in a Python codebase, or when the ecosystem forces it (data science, ML, a Python-only library). + +# Rule: Project Skill Symlinks + +Keep the real skill in `.agents/skills//` and treat `.claude/skills/` as a symlink to it, not a second copy. Create the Claude entry with `ln -s ../../.agents/skills/ .claude/skills/` so both locations point at the same source of truth. + +# Rule: Runtime Tool Discovery + +When a workflow references a custom CLI — a local script or anything you may not already know — run ` --help` before first use. The help output is the authoritative source for subcommands, flags, and usage; rules only name the tool and rely on `--help` to teach you the rest at runtime. + +Compose these tools with pipes like any Unix CLI, and prefer machine-readable formats (`--porcelain`, `--json`) over parsing human-readable output. + +# Rule: Repository Scripts + +`scripts/` is the default home for the repository's operational tooling — automation, helpers, and one-offs that maintain the repo but aren't part of what it ships. + +**Look there first.** If `scripts/` exists, `ls scripts/` and `jq '.scripts' package.json` before writing anything new — extend what's there rather than forking it. + +**Put new scripts there by default.** Anything worth committing — release helpers, submodule updates, git hooks, recurring chores — goes in `scripts/`, wired through `package.json` so callers invoke it by name rather than remembering the path. + +# Rule: Set an Explicit Timeout for Long CI Waits + +Waiting on CI to finish — `gh run watch`, `fgj actions run watch`, or a poll loop over `gh run list --commit ` — routinely outlasts an agent shell tool's default command timeout (Claude Code's Bash tool defaults to 120s, raisable to 600s). Pass an explicit longer timeout (e.g. 420000 ms) to the command invocation, or the wait is killed mid-run and reports a false failure. + +# Rule: Sub-Agent Delegation + +Spawn sub-agents liberally. A sub-agent encapsulates a chunk of work behind a simple interface: briefed on the task, it gathers its own context, works autonomously, and hands back only the result — trust the process and engage with the outcome. The main agent's job is to frame tasks, dispatch, and synthesize results. + +**Delegate, move on, verify.** A task of many simple steps is prime delegation material — deploying an Ansible playbook and combing its verbose logs where nearly every task just reports ok, provisioning a throwaway OrbStack VM with Node, Docker, and Postgres installed, clicking through a multi-page web flow filling fields and pressing buttons, watching a CI run, applying a bulk mechanical edit. Hand it to a background sub-agent, move on to other work while it runs, and when it finishes, spawn a fresh sub-agent to confirm the work was done correctly — and for critical work, several adversarial reviewers, each attacking the result through a different lens. + +**Skills delegate too.** Instead of invoking a skill yourself, consider handing it to a sub-agent: name the skill and the inputs, and the sub-agent loads it, follows the workflow, and returns the result — the skill's full instruction set never enters the main context. `agent-browser` is the perfect shape for this: "log in to the site and check that such-and-such feature works" is a one-line instruction with a one-line answer, and everything in between — dozens of tool calls, failed selectors, retries — stays encapsulated in the sub-agent. Whether that fits is a per-skill call. + +# Rule: TSV Parsing + +`awk` splits on any whitespace by default, silently breaking on TSV values containing spaces. For tab-separated output (often `--porcelain` flags), set the delimiter explicitly: + +```bash +# BAD: prints "name" instead of "name with spaces" +printf 'id\tname with spaces\tstatus\n' | awk '{ print $2 }' + +# GOOD — pick one: +awk -F'\t' '{ print $2 }' +cut -f2 # cut defaults to tab +while IFS=$'\t' read -r a b c; do …; done +``` + +Empty fields are a second trap for the `read` form only: tab is IFS _whitespace_, so runs of tabs collapse into one delimiter and an empty middle field shifts every value after it — `awk -F'\t'` and `cut -f` are immune. When a field can be empty, prefer `awk`/`cut`, keep nullable fields last, or in zsh double the tab (`IFS=$'\t\t'`), which the manual defines as demoting it to a hard delimiter that preserves empty fields; bash has no doubled form — it silently ignores the doubling and shifts the fields anyway, so a bash-run copy of the zsh idiom reinstates the exact bug it exists to prevent, with no diagnostic. + +Not every `--porcelain` is TSV — `git worktree list --porcelain` is space-separated per line. Sample output before picking a delimiter. + +# Rule: Reach for Unix-Native Primitives Before Inventing Abstractions + +Use the OS as the first control plane. Before proposing a registry, supervisor, scheduler, logger, IPC layer, config store, or discovery protocol, check whether argv, environment variables, inherited file descriptors, filesystem paths, Unix-domain sockets, ports, signals, stdout/stderr, cron, systemd, or XDG paths already solve it. + +Two primitives cover almost everything: + +- **Handoff at fork/exec** — parent passes addresses to children via args, env vars, or inherited FDs (`SSH_AUTH_SOCK`, systemd socket activation). +- **Well-known names in a shared namespace** — filesystem paths and TCP/UDP ports (`/var/run/docker.sock`, port 22). The filesystem is the service directory. + +Common problems map directly: process discovery → socket at a conventional path or env var; IPC → Unix-domain socket, named pipe, or signal; supervision → systemd or another init; scheduling → cron or systemd timer; logging → stdout/stderr; config → XDG config dir. + +For Node apps needing a config, data, cache, log, or temp directory, default to [`env-paths`](https://github.com/sindresorhus/env-paths). It returns the right location per platform — XDG on Linux, `~/Library/...` on macOS, `%APPDATA%` on Windows — so you don't hand-roll `process.platform` branches that drift. Pass a namespace (`envPaths('my-app')`) and use the returned `config`, `data`, `cache`, `log`, `temp` paths directly. + +**Don't build a second control plane.** Reject the native primitive only when you can name the concrete property it cannot provide: distributed discovery across hosts, authorization the OS namespace cannot enforce, schema evolution for a long-lived wire format, multiplexing many streams over one transport, binary streaming with backpressure, or cross-platform targets where no equivalent primitive exists everywhere. + +Otherwise the native primitive wins. Designs that don't compose with pipes, signals, and conventional file locations pay a tax forever. + +# Rule: Use Native TypeScript Execution + +Use Node 24+ and run `.ts` files directly with `node script.ts`. Node strips types at runtime — no `tsx`, no `ts-node`, no `tsc` build step. + +Default to `.ts` over `.mjs` for new scripts and to `node` over `tsx` in `package.json`. diff --git a/package.json b/package.json index 11f7b50..5300e65 100644 --- a/package.json +++ b/package.json @@ -71,5 +71,8 @@ "engines": { "node": ">=24.0.0" }, - "packageManager": "pnpm@11.8.0" + "packageManager": "pnpm@11.8.0", + "j4kAlignIgnorePatterns": [ + "archive/**" + ] }