chore(skills): embed repo skills in .agents/skills

.agents/skills is the vendor-neutral Agent Skills location that Copilot,
Codex, Claude Code, and most other agents discover automatically, while
.github/skills is read by Copilot alone. Move the embedded skills there
and add project-knowledge, a committed per-topic memory of verified
gotchas (codebase, shells, terminal, testing).

The directory stays gitignored for apm-managed skills; only the three
embedded skills are allowlisted, mirroring the former .github/skills
pattern. AGENTS.md now tells agents to read the relevant topic before
starting work and to commit new findings alongside the change they
relate to.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 70fe8f468dbe
This commit is contained in:
Jan De Dobbeleer
2026-07-14 11:36:46 +02:00
committed by Jan De Dobbeleer
co-authored by Claude Fable 5
parent 0a0774dff3
commit 60232851e0
14 changed files with 423 additions and 8 deletions
+48
View File
@@ -0,0 +1,48 @@
---
name: project-knowledge
description: >
Accumulated project memory: verified gotchas and prior findings for oh-my-posh work. Consult
BEFORE touching shell integration scripts (zsh, pwsh, fish, bash, cmd/Clink), terminal or pty
behavior, WSL-based shell testing, or internals (cache, segments, streaming, serve daemon).
Read only the topic files relevant to the task at hand.
---
# Project Knowledge
A modular memory of hard-won, verified facts about this codebase and its runtime environments.
Everything here was learned the expensive way - through debugging, benchmarking, or reproduction -
and is not derivable from a quick read of the code.
## How to use
1. Identify which topics the task touches (a zsh script change touches `zsh` and probably
`testing`; a segment bug touches `codebase`).
2. Read those reference files before writing code or designing an experiment.
3. Treat entries as point-in-time observations: they carry dates, and code moves. Verify a claim
against the current code before building on it, and fix the entry when it drifted.
## Topics
| Topic | Read when |
| ------------------------------------ | ---------------------------------------------------------------------- |
| [codebase](references/codebase.md) | Touching Go code: segments, cache, templates, streaming, serve daemon |
| [zsh](references/zsh.md) | Touching `omp.zsh`, zle widgets, coproc, or zsh plugin interop |
| [pwsh](references/pwsh.md) | Touching `omp.ps1`, PSReadLine, runspaces, events, or pwsh perf |
| [fish](references/fish.md) | Touching `omp.fish`, fish jobs, fifos, or fish event handlers |
| [bash](references/bash.md) | Touching `omp.bash`, PROMPT_COMMAND, readline, or bash coprocs |
| [cmd-clink](references/cmd-clink.md) | Touching `omp.lua`, Clink integration, or Windows pipe lifecycles |
| [terminal](references/terminal.md) | Reasoning about ptys, ConPTY, Windows Terminal, or terminal encoding |
| [testing](references/testing.md) | Building a harness to drive a shell end-to-end (WSL, zpty, script(1)) |
## How to maintain
This is a living memory - extend it whenever a session ends with knowledge worth keeping:
- Add durable, **verified** facts only: gotchas, platform quirks, measured numbers, failed
approaches worth not retrying. No speculation, no session-specific state.
- Date non-obvious claims (`verified 2026-07-14`) so future readers can judge staleness.
- One topic per file. Append to the matching reference file; create a new file and index row when
a fact fits no existing topic.
- Prefer updating or deleting a stale entry over stacking corrections on top of it.
- Keep entries self-contained: name the file, function, or command they apply to.
- This skill is committed - include knowledge updates in the commit of the change they relate to.
@@ -0,0 +1,34 @@
# bash
Verified on bash 5.2 (2026-07).
## Readline and prompt
- Running the `exec` builtin from PROMPT_COMMAND - even a harmless `exec {fd}</dev/null` -
**silently disables readline for the whole session**: the prompt never paints again, no error,
commands still execute non-interactively. Use coproc fds directly; never exec-dup or exec-close
from prompt context.
- PS1 must stay `'$(_omp_get_primary)'` (single-quoted). With promptvars on, literal prompt
content executes `$(...)` - a directory named `$(cmd)` becomes command injection.
- "Expansion returns correct bytes" is NOT "prompt displays" - always capture a real typescript
(`script -qe -c ... FILE`) when verifying prompt behavior. Note `script(1)`'s stdout relay does
not carry bash prompt bytes (zsh's does); probe `${PS1@P}` in-session instead.
## coproc
- `coproc NAME { cmd; }` reports the pid of a wrapper subshell - `exec` in the body so the daemon
replaces it.
- The coproc's ORIGINAL fds must be closed after duplication
(`eval "exec ${NAME[0]}<&- ${NAME[1]}>&-"`) or the child's stdin never EOFs when the dups close.
- Subshells close coproc fds in non-interactive bash - a `(trap '' PIPE; ...)` subshell guard does
not work there; save/ignore/restore `trap '' PIPE` in the parent instead (nothing forks while
ignored).
- bash prints failed `>&fd` redirection errors before later redirections apply - put
`2>/dev/null` BEFORE `>&"$fd"`.
## History
- A bash serve daemon was implemented and **reverted** (2026-07-07): no measurable speedup -
native-Linux spawns are 11-16ms and sync-only wait-mode plus the display-time subshell cannot
beat that. Do not re-propose without new evidence. The gotchas above came out of that work and
remain valid.
@@ -0,0 +1,25 @@
# cmd / Clink
## Lua and pipes
- Clink's `io.popenrw` reads are BLOCKING with no peek/timeout in Clink Lua (verified in
`io_api.cpp`). Any protocol consumed from Lua must guarantee a fixed number of records per
request - the serve wait-mode contract (exactly 2 records, upheld by Go's `renderComplete` even
on segment panic) exists for this.
- `io.popenrw` runs the command via `%COMSPEC% /c`, so `2>nul` works in the command string and is
REQUIRED - the child's stderr otherwise inherits the console and corrupts the display.
- Clink creates its pipes `_O_NOINHERIT` and only dups the child ends inheritable
(`pipe_pair::init`), so cmd's death guarantees the daemon's stdin write handle closes.
## Windows lifecycle
- There is no SIGPIPE on Windows - stdin EOF is a daemon's ONLY exit signal. Design teardown
around fd closure, not signals.
- The cmd feature line for the daemon is `serve_enabled = true` (Streaming feature in
`src/shell/cmd.go`).
## Testing
- `luac -p` for syntax; a Lua harness with a stubbed Clink API covers logic
(lua.exe via `winget install DEVCOM.Lua`, Clink via `winget install chrisant996.Clink`).
- Clink itself cannot run headless - live smoke tests stay manual.
@@ -0,0 +1,62 @@
# Codebase
## Dev environment
- The Go module root is `src/`, not the repo root - run all `go` commands from there.
- On windows/arm64 dev machines `go test -race` is NOT supported. Concurrency-sensitive changes
must rely on CI (amd64) for race detection.
- Rendering hot-path benchmarks live in `src/template/bench_test.go`,
`src/terminal/bench_test.go`, and `src/prompt/bench_test.go`; compare runs with `benchstat`.
- `template.Init` resets the parsed-template cache - a macro benchmark that calls it per iteration
measures the cold-parse path, not steady state.
## Shell integration scripts
- Everything under `src/shell/scripts/` is **embedded at build time** (`go:embed`). After editing a
script, rebuild the binary before testing; `oh-my-posh init <shell> --print` shows the generated
output and is the fastest way to inspect what a user actually sources.
- Features (transient prompt, tooltips, vi mode, streaming) are emitted per shell from
`src/shell/<shell>.go` - a script function is dead code unless the feature switch emits its
activation line.
## Segments and panics
- Segment `Execute` runs in bare goroutines with **no recover** (`src/prompt/segments.go`), and
template rendering re-panics runtime errors. Any panic there kills the whole process - the user
sees a completely blank prompt. So when a user reports a blank prompt: find the panic.
- If the panic trigger persists (e.g. a poisoned cache entry with a TTL), every prompt crashes
until the entry expires.
- Segment writers gob-encode only exported fields. `segments.Base.env/options` are unexported and
MUST survive a cache restore: overlay the restored data onto the writer initialized by
`MapSegmentWithWriter`, never replace the writer.
## Cache
- Cache persistence only happens with the hidden `--save-cache` flag (print/stream commands);
without it, stores never write on close. Redirect the location with `OMP_CACHE_DIR`.
- Debug logs are buffered and only printed by the `oh-my-posh debug` command (grep for
`restored segment from cache` / `setting entry`). `POSH_TRACE=1` and stderr show nothing for
print commands.
- On Windows the cache file is a memory-mapped 50KB+5 "persistent shared string" with a 4-byte
length header; a fresh file is all zeros and logs a harmless `store.go:init EOF` error on first
read.
## Streaming and serve daemon
- Streaming is enabled by the top-level `"streaming": <ms>` config key. That value is ALSO each
segment's pending-timeout and overwrites segment-level `timeout`.
- `stream` always emits the transient prompt as a `\x1e`-prefixed NUL record (initial + refreshed
once all segments resolve); serve records are `<id>\x1f<payload>\0`.
- Serve pitfall class: process-lifetime initializers (`if X != nil return`) pin first-render state
in a daemon. `template.Cache` did exactly that (pinned PWD/Folder/Code/Jobs) - fixed with
`template.ResetCache()` per render in `startRenderCycle` (flag-based rebuild; never nil the
global, abandoned segment goroutines may still read it). Audit for this pattern when extending
serve.
- Do NOT memoize the config in serve: the per-render gob decode is load-bearing - a fresh segment
graph per cycle isolates the active render from abandoned-cycle goroutines holding pointers into
their own graph.
- Daemon tests must vary per-request context (cwd, status) across cycles; single-context tests
cannot catch one-shot-assumption state.
- `config.Get` prefers the session gob cache over `POSH_THEME`.
- Go guarantees exactly 2 records per wait-mode serve request even on segment panic
(`renderComplete`) - blocking clients (Clink) rely on this.
@@ -0,0 +1,29 @@
# fish
Verified on fish 4.1.2 in WSL (2026-07).
## Process and job model
- fish does NOT fork a backgrounded pipeline stage that is a *function* - it blocks the main
shell. Run readers as an external process: `fish --no-config -c $script args &`.
- fish 4.x `jobs --last --pid` prints a "Process" header plus one pid per pipeline stage - filter
with `string match --regex '^\d+$'` and treat the result as a list.
- `kill -0 0` always succeeds (signals the caller's own process group) - guard zero pids
everywhere before liveness checks.
## Fifos and lifecycle
- A fifo write with no reader blocks forever - liveness-check the reader before every fifo write.
- The serve daemon opens its request fifo O_RDWR so fish's open-write-close pattern never EOFs it;
consequence: SIGKILL of fish orphans daemon and reader permanently (no EOF, no SIGPIPE ever).
Normal teardown runs via the `fish_exit` event handler (quit + kill the pipeline).
- `--on-signal SIGUSR1` handlers fire between commands in non-interactive scripts (verified with a
200ms poll loop).
## Testing quirks
- fish under `script(1)` discards/paste-buffers piped typeahead, so in-session probes never
execute - a harness limitation, not a bug (zsh under the same setup is fine). Assert via files
written by event handlers instead.
- The streaming transient prompt is cached in a tempfile (`$_omp_streaming_tempfile.transient`),
not a variable, because `_omp_cleanup_stream` runs before the transient repaint.
@@ -0,0 +1,57 @@
# PowerShell (pwsh)
## Engine events and runspaces
- `Register-EngineEvent` (e.g. `PowerShell.OnIdle`) `-Action` handlers, verified pwsh 7 (2026-07):
- `-MessageData` is accepted but arrives as `$null` in the action (unlike `Register-ObjectEvent`,
where it works).
- `.GetNewClosure()` bindings are lost when the scriptblock is created inside a module function -
captured variables resolve to `$null` when the action fires.
- The only reliable way to pass state into an engine-event action is a `$global:` variable
(see `$global:_ompStreamingState` in `src/shell/scripts/omp.ps1`).
- PSReadLine generates `PowerShell.OnIdle` itself every ~300-450ms while waiting for input (only
when the input buffer is empty) and pumps ALL queued subscriber actions via a nested pipeline.
`InvokePrompt()` is designed for OnIdle subscribers.
- PSEvents raised from a NON-engine thread (e.g. `Register-ObjectEvent DataAdded` on a collection
appended by a background runspace) can re-enter `RunspaceBase.Pulse()` and **crash the host**
with InvalidPipelineStateException under rapid prompt cycles. Consume records only on the engine
thread: sync waiter plus OnIdle drain.
- Whether `Register-ObjectEvent` actions fire during a busy-wait is CONTEXT-DEPENDENT. Never share
a consume-cursor between an event action and a synchronous waiter - give the waiter a private
cursor (records stay in the PSDataCollection) and make the action idempotent.
- OnIdle needs ~300ms of idle time. Anything the user can trigger sooner (transient prompt on a
fast Enter) must ALSO drain synchronously at its call site or it pays a CLI-spawn fallback.
## Exit lifecycle
- pwsh cannot exit while a `[powershell]::Create()` pipeline thread runs - pipeline threads are
foreground threads. A reader runspace blocked in `ReadByte()` on a child's stdout deadlocks
`exit` when that child only terminates on stdin EOF (issue #7643).
- Module `OnRemove` only runs on `Remove-Module`, never on normal `exit` - it cannot be a
daemon's teardown path. Use a `Register-EngineEvent PowerShell.Exiting` handler that writes
`quit`, **closes the child's stdin** (the guaranteed EOF signal), and kills after a short
`WaitForExit`. State reaches the action via `$global:` (see above).
## Performance (measured 2026-07-06, ARM64 Windows 11)
- Process creation floor is ~70ms (`cmd /c exit`); any omp spawn from pwsh costs ~100-130ms wall
regardless of exe speed. When touching `omp.ps1` perf, count process spawns per Enter first -
the spawn-per-prompt architecture dominates, script/module cmdlet overhead is negligible
(<0.1ms; module vs plain script is a non-issue).
- `&` call operator vs the Process API is only ~10-17ms faster - CreateProcess dominates. Under a
stock CP437/1252 console, `&` mangles UTF-8 output (U+E0B6 becomes U+03B5); dropping the Process
API requires `[Console]::OutputEncoding = UTF8` once at init.
- Serve daemon warm render ~10ms; warm prompt cost 18-23ms vs 160-220ms for the per-prompt
streaming cycle it replaced (runspace creation ~35ms + event churn + 15.6ms-quantized sleeps).
- Windows sleep granularity: each `Start-Sleep -Milliseconds 1` is a ~15.6ms tick - sleep-polling
loops are ~16x slower than intended. Prefer a `ManualResetEventSlim` signaled by the reader.
## Testing
- `New-Event -SourceIdentifier PowerShell.OnIdle` runs the real OnIdle action deterministically
(engines never idle mid-script). Beware the action's side effects short-circuiting later prompt
calls in the harness.
- Set module-scope flags from a test: `& (Get-Module oh-my-posh-core) { $script:X = $true }`.
- pwsh 5.1 / ConstrainedLanguage keep the legacy stream path - serve is gated to pwsh 6+.
- Exit-deadlock repro pattern: `Start-Process pwsh -File test.ps1` + `WaitForExit(timeout)`;
`findstr x` with redirected stdio is a good stand-in daemon.
@@ -0,0 +1,30 @@
# Terminal
## Windows Terminal and ConPTY
- The "[process exited with code 0] / Ctrl+D to close" text flashing at shell exit is **Windows
Terminal's own teardown message** (`ConptyConnection::_indicateExitWithStatus`) - visibility is
a render/close race, never omp output.
- Panes launched via `wt.exe <commandline>` never auto-close under `closeOnExit=automatic`;
profile-launched tabs do.
- On Windows 11 build 26300 a headless conhost reports cursor 0,0 / blank buffer and drops pty
pipe input - visual row placement cannot be asserted headlessly, only state-machine behavior.
- pywinpty's ConPTY teardown EOF lags process death by a constant ~5s (harness artifact - it shows
up in a plain-pwsh control too).
- SendKeys into Windows Terminal is unreliable while the user works - keystrokes get silently
lost, so a "hung" test may simply never have received its `exit`.
## Encoding
- Under a stock CP437/ACP1252 console, pwsh's `&` call operator mangles UTF-8 program output
(U+E0B6 becomes U+03B5). Reading a native program's UTF-8 output reliably requires
`[Console]::OutputEncoding = UTF8` or the Process API.
- PowerShell `| Set-Content -NoNewline` joins piped lines into ONE line and corrupts scripts -
write files with Git Bash redirection when byte fidelity matters.
## Process facts
- Windows process creation floor is ~70ms; a timer tick is 15.6ms (quantizes every short sleep).
- On Windows there is no SIGPIPE; child lifecycle management must rely on fd closure / stdin EOF.
- On native Linux, process spawns cost 11-16ms - daemon architectures that pay off on Windows can
be a wash there (see the bash serve revert in [bash](bash.md)).
@@ -0,0 +1,55 @@
# Testing shells end-to-end
Patterns for functionally driving omp's shell integrations, mostly in WSL (verified on aarch64,
zsh 5.9, fish 4.1.2).
## WSL basics
- WSL `/tmp` is wiped between separate `wsl.exe` invocations (instance auto-shutdown). Either make
a test fully self-contained in ONE `wsl -e` call, or stage everything under `$HOME` (e.g.
`~/omp-test`).
- From Git Bash, prefix `MSYS_NO_PATHCONV=1` so `/mnt/c/...` arguments reach wsl.exe unmangled.
- Build for WSL either inside WSL (`/usr/local/go/bin/go`) or cross-compile from Windows
(`GOOS=linux GOARCH=arm64`). Shell scripts are embedded at build time - rebuild after every
script edit, then regenerate the sourced init with `oh-my-posh init <shell> --print`.
- Differential testing: build the control binary from `git show HEAD:<file>` or via
`git stash push -- <file>`; the fixed binary from the working tree.
- `pkill -f <pattern>` matches its own caller's command line when inlined - pick patterns that
cannot appear in the runner script.
## Getting a pty
- `zsh -i` under plain `wsl -e` hangs before the first prompt (no foreground pty). Options:
- `wsl -e script -qec 'zsh -i' /dev/null` - real pty, MONITOR enabled, job-control behavior
testable. Run backgrounded with output to a file and poll a done-marker; foreground runs can
wedge under the wsl relay.
- `zmodload zsh/zpty` in a driver zsh - best for keystroke-level scenarios: `zpty omp zsh -i`,
write raw keys with `zpty -w -n omp $'\x03'` (Ctrl+C delivers a real SIGINT through the pty),
assert via state files written by in-session commands, drain output with `zpty -r` after exit.
Note `script(1)` does NOT work inside zpty (typescript never appears).
- Per-shell `script(1)` quirks: bash prompt bytes are not relayed to stdout (probe `${PS1@P}`
in-session); fish discards piped typeahead (in-session probes never run); zsh relays fine.
- Do not test the zsh serve path from a non-interactive `zsh script.zsh` - it hangs in
`read -u fd -d $'\0' -t N` (the `-t` is ignored with `-d`, see [zsh](zsh.md)).
## Driving vi-mode / keystroke scenarios (zpty pattern, verified 2026-07-14)
- Harness pattern (built for #5992): stage a binary, config, zdot dirs, and zpty driver scripts
under `$HOME` in WSL; one driver runs mode-aware ESC/Enter/Ctrl+C scenarios into a state log,
another asserts the transient prompt renders. Always compare a fixed run against a control zdot
(same setup, feature under test disabled).
- Keep probes mode-aware: with zsh-vi-mode, a line after a normal-mode accept starts in normal
mode - prefix typed commands with `i` where needed, and remember a stray self-inserted `i`
turns the probe into a failing command (useful as a broken-state detector: `last_status=127`).
## Keeping a render alive
- There is no shell-command segment type. To hold `omp stream`/a render open for a controllable
window, point an `http` segment at a silent local TCP listener
(`python3 -c 'socket...accept...sleep'`) with a large `http_timeout`; kill the listener to
trigger the async update record.
## Reading protocol streams
- One `bufio.Scanner` per pipe, ever - a second scanner on the same pipe loses buffered data
(use a shared reader helper in Go tests).
@@ -0,0 +1,53 @@
# zsh
## zle facts
- Every new editor invocation starts in keymap `main`; a `vicmd` selection does not survive into
the next line.
- `$?` after the transient prompt's `zle .send-break` is 1; native Ctrl+C yields 130. Known gap,
documented as a TODO in `_omp_zle-line-init` - do not "fix" one without solving the other path.
- Ubuntu's `/etc/zsh/zshrc` defines a `zle-line-init` (terminfo smkx), so omp's widget takes the
decorate path even in minimal setups - never assume the widget slot is empty.
## zsh-vi-mode (ZVM) interaction (verified 2026-07-14, issue #5992)
- ZVM initializes lazily at first `precmd`, so it ALWAYS wraps omp's `zle-line-init` regardless of
source order: its wrapper runs our widget first, `zvm_zle-line-init` second. Because the
transient prompt's `zle .recursive-edit` consumes the whole editing session, ZVM's line-init
effectively ran at line END - any line accepted or interrupted from normal mode desynced
`ZVM_MODE` from the active keymap, and `zvm_select_vi_mode`'s same-mode early return made the
break permanent.
- Fix in `_omp_zle-line-init`: call `zvm_zle-line-init` up front (guarded on
`$+functions[zvm_zle-line-init]` and `ZVM_INIT_DONE == true`). Keep this if the function is ever
restructured.
- Landmine: `zvm_reset_prompt` resolves `$rawfunc` via **dynamic scoping**. Any ZVM code running
`zle reset-prompt` while inside our line-init picks up the line-init wrapper's `rawfunc`
(`_omp_decorated_zle-line-init`) and re-enters the widget recursively. The `local rawfunc=` at
the top of `_omp_zle-line-init` shadows it - load-bearing, keep it.
## coproc and signals
- An interactive zsh with MONITOR prints "[n] pid" at coproc spawn; `disown` is too late and a
`{ coproc ... } 2>/dev/null` block does NOT suppress it. `setopt localoptions no_monitor` does -
side effect: the child inherits SIGINT/SIGQUIT ignored (POSIX no-job-control), which Go
preserves.
- Duplicate coproc fds to session fds (`exec {out}<&p {in}>&p`) - duplicates survive a later
`coproc` replacing the slot. `disown %+` keeps the daemon out of `jobs` and the job-count
segment.
- Writing to a dead coproc pipe raises SIGPIPE, which **kills a non-interactive zsh outright**
(`2>/dev/null` cannot stop a signal). Guard daemon writes with a `kill -0 $pid` pre-check plus
`setopt localoptions localtraps; trap '' PIPE` (function-local, user pipelines unaffected).
- Never pass a possibly-zero pid to `kill -0` - `kill -0 0` signals the caller's own process group
and always succeeds.
## Footguns
- A redirection-only `exec` applies EVERY listed redirection to the shell permanently:
`exec {fd}<&p {fd}>&p 2>/dev/null` silences the session's stderr for good (caused issue #7653).
Scope the stderr suppression with a brace block: `{ exec ... } 2>/dev/null`.
- zsh 5.9: `read -r -u $fd -d $'\0' -t N` ignores `-t` entirely and blocks forever on a silent fd.
Without `-d`, the timeout works.
- Teardown belongs in `zshexit_functions`; the daemon lifecycle is fd-governed (closing the fds -
or the shell dying, even by SIGKILL - EOFs the daemon's stdin).
- To debug widget re-entry, log `${funcstack[*]}` inside the widget - `zle` calls from shell
functions appear on the stack and expose who invoked what.
+5 -4
View File
@@ -1,11 +1,12 @@
# APM
apm_modules/
.github/skills/*
.github/instructions/*
!.github/skills/segment-create/
!.github/skills/segment-docs/
.github/instructions/*
.agents/*
!.agents/skills
.agents/skills/*
!.agents/skills/segment-create/
!.agents/skills/segment-docs/
!.agents/skills/project-knowledge/
# Others
+23 -2
View File
@@ -1,6 +1,6 @@
# GitHub Copilot Instructions
# Agent Instructions
For general coding guidelines, commit conventions, and agent workflows, see [AGENTS.md](../AGENTS.md).
General coding guidelines, commit conventions, and agent workflows for this repository.
## Tech Stack
@@ -66,6 +66,27 @@ Themes are plain JSON files in `themes/`. New themes must validate against
`website/static/schema.json`. Do not introduce breaking schema changes without updating the
schema file.
## Skills
Agent skills live in `.agents/skills/` - the vendor-neutral Agent Skills location that Copilot,
Codex, Claude Code, and most other agents discover automatically. Most skills are installed via
APM (see [CONTRIBUTING.md](CONTRIBUTING.md)) and gitignored; the repository embeds three of its
own: `segment-create`, `segment-docs`, and `project-knowledge`.
## Project Knowledge
The `project-knowledge` skill (`.agents/skills/project-knowledge/`) is the project's durable
memory: verified gotchas about the codebase, shells, terminals, and test harnesses. Before working
in any of those areas, read the matching topic file - it exists to keep you out of known rabbit
holes.
Reading it is half the contract; writing to it is the other half. When a session uncovers
something a future session should know before going down the same rabbit hole - a platform quirk,
a non-obvious root cause, a failed approach worth not retrying - append it (dated, verified) to
the matching file in
`.agents/skills/project-knowledge/references/`. Create a new topic file plus an index row in its
`SKILL.md` when none fits. Commit the knowledge update together with the change it relates to.
## Pull Request Reviews
Whenever any agent performs or addresses a pull request review, follow this process at all
+2 -2
View File
@@ -14,8 +14,8 @@ can be a good starting point.
## Setting Up Agents and Skills
This project uses [APM (Agent Package Manager)][apm] to manage shared AI agent skills.
Project-specific skills live in `.github/skills/`, while shared skills are declared
in `apm.yml` and installed via APM.
Project-specific skills are embedded in `.agents/skills/`, while shared skills are declared
in `apm.yml` and installed via APM into the gitignored parts of that same directory.
### Install APM