feat: operating contract (AGENTS.md) + KB conformance linter (scripts/kb-lint.sh)
This commit is contained in:
1 parent
02883d3ed2
commit
5e051f1ce9
2 files changed
+130
No files matched your search
@@ -0,0 +1,79 @@
|
|||||||
|
# AGENTS.md — ~/.omp command center
|
||||||
|
|
||||||
|
System-configuration command center for Frank's fleet: agent config, knowledge base,
|
||||||
|
small system utilities, and (soon) dotfiles. You — the reader — are probably an agent
|
||||||
|
running on Dankware with this repo as your working directory. Read
|
||||||
|
`omp-docs/index.md` before you touch anything. This file is the operating contract;
|
||||||
|
`omp-docs/` is the knowledge base (OKF v0.1 format: directory of markdown files
|
||||||
|
with YAML frontmatter, spec at `omp-docs/references/okf-spec.md`).
|
||||||
|
|
||||||
|
## What this repo is
|
||||||
|
|
||||||
|
- The live agent-config directory AND a git repo pushed to Gitea
|
||||||
|
(`dank/command-center` on the Dankclaw instance). There is no staging copy:
|
||||||
|
**the checkout is the deployment.** Branch switches swap live config files out
|
||||||
|
from under the running agent — keep feature branches short-lived, and never end
|
||||||
|
a work session checked out on anything but `develop` or `main`.
|
||||||
|
- Contents that belong here: `AGENTS.md`, `omp-docs/` (knowledge base),
|
||||||
|
`scripts/` (small system utilities), `agent/` + `plugins/` (harness config and
|
||||||
|
plugin manifests). Everything else under this directory is runtime state and is
|
||||||
|
gitignored on principle — see `.gitignore`. History must mean "the configuration
|
||||||
|
changed", not "the agent breathed".
|
||||||
|
|
||||||
|
## Git-flow SDLC (full flow, from day one)
|
||||||
|
|
||||||
|
- Work item = Gitea issue on `dank/command-center` (Gitea at 192.168.0.143:3000,
|
||||||
|
owner `dank`). Small fixes may go straight to a branch; anything with more than
|
||||||
|
one moving part gets an issue first.
|
||||||
|
- Branch naming: `<type>/<issue#>-<slug>`, type ∈ feat|fix|docs|chore.
|
||||||
|
Release cuts: `release/X.Y.Z` from `develop`; hotfixes: `hotfix/<slug>` from `main`.
|
||||||
|
- **Merge, never rebase**, when updating a branch against a moved main — Frank
|
||||||
|
dislikes the history rewrite. **Never merge a PR without an explicit OK from Frank.**
|
||||||
|
- Merge into `develop` with `--no-ff` so feature arcs stay visible in history.
|
||||||
|
- Releases: cut `release/X.Y.Z` from `develop`, verify, merge to `main`, tag
|
||||||
|
`vX.Y.Z`, back-merge `main` into `develop`. Tags are the deploy markers every
|
||||||
|
other machine pulls to.
|
||||||
|
- Trash convention: never `rm` anything user-authored — move it to `.AGENTTRASH/`
|
||||||
|
(gitignored). Deletion is a pruning session with Frank present.
|
||||||
|
|
||||||
|
## Secret gate (HARD rule)
|
||||||
|
|
||||||
|
The pre-commit hook (`.githooks/pre-commit`, enabled via
|
||||||
|
`git config core.hooksPath .githooks`) scans every staged diff for live-secret
|
||||||
|
shapes (`sk-…`, `ghp_…`, `github_pat_…`, `glpat-…`, `xox…`, `AKIA…`, PEM headers,
|
||||||
|
JWTs) and blocks the commit. Quarantine list — never tracked, ever: `auth.json`,
|
||||||
|
`.env`, `trust.json`, `*.pem`, `*.key`, `identity*`. Deliberate override:
|
||||||
|
`ALLOW_SECRETS=1 git commit …`. Remember why: a secret in a tracked file is a
|
||||||
|
secret in every agent's context window on every machine that pulls this repo.
|
||||||
|
|
||||||
|
## Machine facts (verify before trusting)
|
||||||
|
|
||||||
|
| box | role | GPU | notes |
|
||||||
|
|-----|------|-----|-------|
|
||||||
|
| Dankware | dev laptop, never runs inference | Arc iGPU + RTX 4070 Max-Q (dGPU, non-unified) | 22 threads; mainline llama.cpp at `~/GitPackages/llama.cpp/build/bin/`; this repo lives here at `~/.omp` |
|
||||||
|
| Dankclaw | bench target, runs the large serving model | Strix Halo APU — **unified memory** | ROCmFPX fork at `~/GitPackages/ROCmFPX/build-qwen38/bin/`; prod service `frank-default-large`, port 7770; ssh: `ssh dankclaw` |
|
||||||
|
| Dankserver | FreeToken box | RTX 3090 dGPU — non-unified | runs `frank-default-small`; ssh: `ssh dankserver` |
|
||||||
|
|
||||||
|
## Environment gotchas
|
||||||
|
|
||||||
|
- Gitea API needs a token: `GITEA_TOKEN` env (gitignored `.env`). No `tea` CLI
|
||||||
|
installed anywhere. Remote URLs embed the token locally in `.git/config` — that
|
||||||
|
file is never pushed; it is not a substitute for the secret gate.
|
||||||
|
- Passwordless sudo (`sudo -n`) works on Dankclaw and Dankserver for systemctl
|
||||||
|
verbs only; user units need no sudo at all.
|
||||||
|
- SSH into both servers is BatchMode-capable from here:
|
||||||
|
`ssh -o BatchMode=yes dankclaw '<read-only command>'` — read-only probes are
|
||||||
|
always allowed; state changes on remote boxes are not, without a per-run OK.
|
||||||
|
- `frank-default-large` on Dankclaw is a **production service** shared with this
|
||||||
|
repo's operator: never stop, mask, or restart it (or any production service on
|
||||||
|
any box) without an explicit, current, per-run OK from Frank.
|
||||||
|
|
||||||
|
## File-organization doctrine (why this repo won't rot like ~/.pi did)
|
||||||
|
|
||||||
|
- One concept, one file; directories get an `index.md` listing their contents —
|
||||||
|
that's the OKF contract, and it is also the anti-rot mechanism: nothing may
|
||||||
|
appear in a directory that its `index.md` doesn't list.
|
||||||
|
- Root of the repo is capped: config, docs, scripts, harness dirs. A new
|
||||||
|
top-level entry requires a commit message that justifies itself.
|
||||||
|
- Runtime state (dbs, logs, caches, sessions) is gitignored on principle — if you
|
||||||
|
feel the urge to commit one of those, stop and reread this section.
|
||||||
Executable
+51
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# kb-lint.sh — OKF v0.1 conformance + link-integrity lint for a knowledge bundle.
|
||||||
|
# Usage: scripts/kb-lint.sh <kb-dir> (default: omp-docs)
|
||||||
|
# Exits 0 when the KB is conformant; prints one line per violation otherwise.
|
||||||
|
set -uo pipefail
|
||||||
|
KB="$(cd "${1:-omp-docs}" 2>/dev/null && pwd)" || { echo "kb-lint: no such dir: ${1:-omp-docs}" >&2; exit 2; }
|
||||||
|
|
||||||
|
python3 - "$KB" <<'PY'
|
||||||
|
import re, sys
|
||||||
|
from pathlib import Path
|
||||||
|
kb = Path(sys.argv[1])
|
||||||
|
violations = []
|
||||||
|
|
||||||
|
for p in sorted(kb.rglob("*.md")):
|
||||||
|
rel = p.relative_to(kb)
|
||||||
|
text = p.read_text(errors="replace")
|
||||||
|
if p.name == "index.md":
|
||||||
|
# OKF rule: index files are directory listings and carry NO frontmatter.
|
||||||
|
if text.startswith("---"):
|
||||||
|
violations.append(f"{rel}: index.md must not have frontmatter (OKF spec)")
|
||||||
|
continue
|
||||||
|
if not text.startswith("---"):
|
||||||
|
violations.append(f"{rel}: missing frontmatter")
|
||||||
|
continue
|
||||||
|
end = text.find("\n---", 3)
|
||||||
|
fm = text[3:end] if end != -1 else ""
|
||||||
|
if not re.search(r"^type:", fm, re.M):
|
||||||
|
violations.append(f"{rel}: frontmatter lacks required 'type:'")
|
||||||
|
|
||||||
|
# link integrity: fenced code blocks hold example paths, not live links — strip first.
|
||||||
|
# Vendored spec is exempt: its example paths are illustrations, not live links.
|
||||||
|
for p in sorted(kb.rglob("*.md")):
|
||||||
|
if str(p.relative_to(kb)) == "references/okf-spec.md":
|
||||||
|
continue
|
||||||
|
text = re.sub(r"^```[^\n]*$^```[^\n]*$", "", p.read_text(errors="replace"), flags=re.M)
|
||||||
|
rel = p.relative_to(kb)
|
||||||
|
for m in re.finditer(r"\]\((?!https?:|#)([^)#\s]+\.md)", text):
|
||||||
|
target = (p.parent / m.group(1)).resolve() if not m.group(1).startswith("/") else (kb / m.group(1).lstrip("/"))
|
||||||
|
if not Path(target).is_file():
|
||||||
|
violations.append(f"{rel}: broken relative link -> {m.group(1)}")
|
||||||
|
for m in re.finditer(r"\[\[([^\]|#]+)", text):
|
||||||
|
# wikilinks resolve by filename anywhere in the KB (Obsidian semantics)
|
||||||
|
name = m.group(1).strip().rsplit("/", 1)[-1]
|
||||||
|
if not list(kb.rglob(name + ".md")):
|
||||||
|
violations.append(f"{rel}: broken wikilink -> [[{m.group(1).strip()}]]")
|
||||||
|
|
||||||
|
for v in violations:
|
||||||
|
print(v)
|
||||||
|
print(f"kb-lint: {len(violations)} violation(s)")
|
||||||
|
sys.exit(1 if violations else 0)
|
||||||
|
PY
|
||||||
Reference in new issue
Block a user