From 0ee08bc368c71781c7c703b731bdf284ecbf4bff Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 18 Oct 2026 18:51:41 -0700 Subject: [PATCH] live: heartbeat sync 2026-10-18T18:51:41-07:00 (Refs #11) --- .../audit-never-run-container-deploy/SKILL.md | 77 ++++++++ .../frank-tts-add-cloned-voice/SKILL.md | 166 +++++++----------- .../gitea-api-pr-operations/SKILL.md | 112 ++---------- .../niri-config-edit-validate/SKILL.md | 2 +- .../SKILL.md | 31 ++++ .../architecture.md | 4 +- .../00mveljelz-vh49.json | 1 + .../state.json | 4 +- .../product.md | 1 + .../state.json | 4 +- .../.local/state/noctalia/settings.toml | 8 +- 11 files changed, 201 insertions(+), 209 deletions(-) create mode 100644 agent/managed-skills/audit-never-run-container-deploy/SKILL.md create mode 100644 agent/managed-skills/skills-cli-retroactive-store-wiring/SKILL.md create mode 100644 agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/queue/01a14c93-8870-75d9-9900-9a731559feef/00mveljelz-vh49.json diff --git a/agent/managed-skills/audit-never-run-container-deploy/SKILL.md b/agent/managed-skills/audit-never-run-container-deploy/SKILL.md new file mode 100644 index 0000000..2334b55 --- /dev/null +++ b/agent/managed-skills/audit-never-run-container-deploy/SKILL.md @@ -0,0 +1,77 @@ +--- +name: audit-never-run-container-deploy +description: "Audit a locally built podman/quadlet deploy path that has never actually run: prove base-image assumptions, env-file reader divergence, and image-hash coverage before the first deploy." +--- + +# Audit a never-run container deploy path + +A deployable that has never been built or started has never been *executed*: every +fault in it is invisible to tests that stub the runtime. Audit it as static code +against the tools it calls, then prove each suspicion cheaply against the real +tool. The faults worth hunting are the ones that **report success while doing +nothing** — those are the ones that survive a green `/health` and a green e2e run. + +## The three fault classes (all measured 2026-10-18, frank-tts-engine) + +1. **A command the base image does not ship.** e.g. `adduser` in a `RUN` line + against a trimmed `ubuntu:24.04` — absent, and not pulled in by + `ca-certificates curl libgomp1`. Dies at build time (loud), but only the first + time anyone builds it. + Prove it in the base, never from memory: + `podman run --rm sh -c "command -v adduser || echo absent; useradd --system --uid 10000 --home-dir /app --shell /usr/sbin/nologin tts && id tts"` + Note the accepted cosmetic warning (`uid 10000 is greater than SYS_UID_MAX 999`) + in a comment, or a future agent "fixes" it by dropping `--system` and the uid + stops matching the sibling container. +2. **Two readers of one env file disagree.** systemd units that pass + `EnvironmentFile=` hand it to podman `--env-file`, which does **no shell + parsing** — it takes the literal remainder of the line — while bash `source` + strips one quoting layer. So `WARM_TEXT="Warm up, one two three."` reaches the + container *with its quotes*, and a script that interpolates it into a JSON body + produces invalid JSON: the container boots, `active`, every voice registered, + every voice cold. Fix in the consumer: strip exactly one matched pair **only** + when both ends are the same quote AND the inner content carries another quote or + backslash (never mangle a value that legitimately contains quotes), then escape. +3. **A build-skip hash that under-covers the image.** `build_compose_image`-style + helpers skip `podman build` when a label hash matches; hashing the `Containerfile` + alone means editing a `COPY`'d app/script rebuilds **nothing** — deploy reports + success, `/health` answers, container serves old code. Each case arm must name + the paths its `Containerfile` copies (`extra=(…)`) and hash Containerfile+extras. + Arms that have no extras keep their old hash, so they do not needlessly rebuild. + +## Test it as behaviour, never as a grep of the handler + +- Drive the **real** function: `awk '/^build_compose_image\(\) \{/,/^\}/' deploy-remote.sh > bci.sh`, + `source` it, put a stub `podman` first on `PATH`, assert the observable question — + *did the deploy ask podman to build, or did it skip?* +- Stub traps that cost real time: podman is invoked `podman image inspect --format …` + so the tag is **`$3`**, not `$2`; tags contain `/` so a state file named after one + needs `${tag//\//_}` or the write silently lands in a nonexistent directory and + every image looks permanently unbuilt; `--label cfhash=…` arrives **before** `-t `, + so collect the label and write it after the arg loop. +- For an env-file bug, write one test that reads the file **both** ways — `source` + and the `--env-file` way (parse `KEY=literal remainder`) — and assert the program + receives the same value in both shapes, including a value that carries its own + quotes and a backslash. A suite that only sources the file is how the bug shipped. +- Prove red: `git show HEAD:scripts/deploy-remote.sh > scripts/deploy-remote.sh`, + run the new test (must fail at the exact intended step), restore. Keep each fix in + its own commit and run the seam at each one so the branch bisects. + +## Base-image and host facts to check while auditing + +- `useradd` lives in `login(1)` (base carries it); `adduser` does not. +- Rootless podman **cannot raise rlimits** — `--ulimit nofile=…` above the host hard + limit dies at container create, not at runtime. +- A referenced `Network=` `.network` file the generator cannot find means the + container unit is **not produced at all**. +- Two containers talk by DNS name only if they attach to a **common** network; + check both unit files rather than trusting a comment that says they share one. +- Container DNS name + the network that owns it, never `host.containers.internal` + or a published loopback port, for service-to-service traffic. + +## Do not + +- Do not deploy the audited thing because the audit is finished: first build of a + heavy image (recursive source clone ~10 min) plus a new unit on a host serving + people is the operator's call. +- Do not add a Prometheus target for a container with no `/metrics` — say the probe + is a decision (blackbox HTTP), do not fake a scrape. diff --git a/agent/managed-skills/frank-tts-add-cloned-voice/SKILL.md b/agent/managed-skills/frank-tts-add-cloned-voice/SKILL.md index 104bea3..d8d5a37 100644 --- a/agent/managed-skills/frank-tts-add-cloned-voice/SKILL.md +++ b/agent/managed-skills/frank-tts-add-cloned-voice/SKILL.md @@ -1,125 +1,81 @@ --- name: frank-tts-add-cloned-voice -description: "Add or repair a cloned voice on the dank fleet: /home/dank/voices/ pack, name rules, boot-warm registration, and the under-a-second proof" +description: "Add or repair a cloned voice on the dank fleet: pack builder, the reference.wav contract, the boot warm hook (argv-cap and env-quote laws), and how to prove a voice really answers." --- -Use for "give frank-tts a new voice", "voice X is missing/unknown", or "the first -request of the day takes 5 s". Runbook of record: vault pages [[Frank TTS]] (voice -packs, naming) and [[Frank TTS Engine]] (the warm registry). Everything below runs -as the deploy user on **Dankclaw** (`ssh -o BatchMode=yes dankclaw '…'`); the -containers are **rootless** (`systemctl --user …`, `podman …` without sudo). +# Adding (or repairing) a cloned voice on the dank fleet -## Naming (the question that gets asked first) +Repo law lives in `serverAppConfigs-docs/Dankclaw/Voice Packs.md` and +`Frank TTS Engine.md` — read those for the pack format. This skill is the +**operating sequence plus the traps that cost real time on 2026-10-18**. -A voice is addressed **by its directory name** — `voice=narrator1` on -`POST /v1/audio/speech` (omp's TTS device may send `voice_id`, same value). Use: +## Stack, fast -- `^[a-z0-9][a-z0-9._-]*$`, **lowercase**, ≤ 64 chars. No slash, no `..`, no - space, no uppercase; must not start with `.`/`_`/`-`. -- Why lowercase rather than just "alphanumeric": the shim accepts - `[A-Za-z0-9][A-Za-z0-9._-]{0,63}` (`app.py`, `VOICE_NAME_RE`) but the **engine - lowercases names**, so `Narrator1` resolves on one side and not the other. The - boot warmer refuses a mixed-case pack loudly instead of half-working. -- Names are user-visible: a 400 carries them in `available_voices`. Fleet - convention is `narrator1`, `narrator2`, … for the open-notebook podcast - speakers — open-notebook is configured with exactly `narrator1/2/3`, so renaming - those breaks podcast rendering, not just TTS — and a lowercase personal name for - a person's own voice. +- `frank-tts-engine` — arm A `qwentts.cpp tts-server`, container-internal `:8880`, + publishes **no port**. Reach it with `podman exec frank-tts-engine curl …`. +- `frank-tts` — OpenAI façade on host `:7772`; resolves voice **by name**. +- Packs: `/home/dank/voices//` with `reference.wav` (+ manifest, `.spk`, + `.rvq`). Builder: `scripts/build-voice-pack.sh` (crop 19.84–25 s, first 10 s of + any source is forbidden). +- Weights: `/home/dank/models/Qwen3-TTS-GGUF/` — + `scripts/download-frank-tts-engine-qwentts.sh [--verify-only]` (1.4 GiB, ~18 s + on the LAN; verifies byte count + Hub LFS sha256). -## Pack layout (host state — never commit it) +## Sequence -`/home/dank/voices//` is a voice **iff** it holds `reference.wav`: +1. Build the pack (dry-run first — sizes are arithmetic and asserted). +2. Deploy the engine only if its files changed: + `./scripts/deploy.sh dankclaw --service frank-tts-engine` + (first run builds the image: ~10 min recursive source clone). +3. Register/warm: `podman exec frank-tts-engine /app/warm-voices.sh` + — registers **every** pack each run (the engine replaces a name it already + holds; `tools/tts-server.cpp:148`), then synthesizes twice per voice. +4. Prove from the consumer side, not the config: + `curl -s localhost:7772/v1/audio/voices` then a real + `POST /v1/audio/speech` and listen to the bytes. -``` -reference.wav 24 kHz mono pcm_s16le, 18-25 s of clean single-speaker audio -reference.spk engine x-vector: enc_dim 2048 f32 = 8192 B -reference.rvq RVQ codes: 22 B per 12.5 Hz frame (5456-6864 B) -manifest.json source path + sha256, crop start/end, engine commit, name -``` +## Trap 1 — the kernel's argument cap (this broke the first deploy) -Build it; do not hand-assemble and never fabricate `.spk`/`.rvq` from a WAV: +`/usr/bin/curl: Argument list too long` in the warm log ⇒ a request body was +passed as **one argv string**. Linux caps a single argument at +`MAX_ARG_STRLEN = 131 072 bytes`; a ~1 MB `reference.wav` base64's to ~1.27 MB. +curl never runs, and hooks report it as `HTTP no response` while `/health` is +green. Fix = write the body to a file and use `--data-binary @file`; feed the +payload through a redirection, not `$(base64 …)`. +Regression-test shape: **real** curl + real listener + a fixture >128 KB, so the +kernel (not a stub) produces the failure. Any pack over ~96 KB trips it. -```bash -scripts/build-voice-pack.sh --all /home/dank/voices # everything in VOICE_PACKS -scripts/build-voice-pack.sh --source SRC --offset SEC --duration SEC --sha256 HEX NAME /home/dank/voices -scripts/build-voice-pack.sh --dry-run ... # validate + crop, no extractor -``` +## Trap 2 — two readers of the env file disagree about quotes -Knobs live in `scripts/voice-packs.env` (one `_SOURCE`/`_OFFSET`/`_DURATION`/ -`_SHA256` set per voice). Three things it enforces that an operator gets wrong: +`bash set -a; source` strips `"…"`. podman's `--env-file` (how the unit really +reads it) does **not**. A value interpolated into a JSON body therefore arrives +quoted and invalidates it. Test both readers, not just `source`. -1. **The 10-second gate.** A crop starting inside the first 10 s of the source is - refused before anything is written (project source-material law), and the - manifest is re-checked against the source's sha256 afterwards. -2. **The effective crop window is 19.84-25 s, not 18-25 s.** An 18.0 s crop is a - legal `reference.wav` but yields only 22 × 225 = 4 950 B of `.rvq`, under the - 5 456 B floor the codec config implies — so the builder says so up front. -3. **It never deletes.** An existing pack is refused unless `--force`, and - `--force` moves the old directory to `$PACKS_DIR/.AGENTTRASH/-/`. +## Trap 3 — locally built images skip rebuilds on a stale hash -Files must be world-readable (the container runs as uid 10000): `644` files, -`755` dirs. `mkdir -p /home/dank/voices` as the deploy user if it is absent — -rootless podman auto-creates a missing bind source **root-owned**, and the -endpoint then sees no voices at all. +`build_compose_image` compares a `cfhash` label; it must cover every file the +`Containerfile` `COPY`s, or an edited `app.py`/hook deploys "successfully" while +the container runs old code. -## Register and warm (the part people forget) +## Measured numbers (2026-10-18, deployed build — retire the trial's ladder) -The engine keeps its voice registry **in memory**, and the **first synthesis of a -registered voice decodes the whole reference clip** to snapshot codec state: -6.2-8.0 s, first audio byte at ~5 530 ms. Later requests restore in 0.5 ms. -Re-registering does **not** warm — only synthesizing does. So: +| thing | value | +|---|---| +| first synthesis of a freshly registered voice | 1 883 ms | +| later short requests | 1 226–1 283 ms | +| ~110-word request (repeat) | 9.4 s / 9.8 s — tracks generated audio, not state | +| whole boot warm, 3 packs | 15.2 s | +| engine resident | ~4.0 GiB | -```bash -# registers every pack that has a reference.wav, then speaks the warm sentence once -podman exec frank-tts-engine /app/warm-voices.sh -systemctl --user --no-pager --full status frank-tts-engine.service | tail -20 -``` +The trial's `6.2–8.0 s cold → 0.5 ms warm` claim does **not** reproduce; warming +is for registration (registry is memory-only, re-register ≠ warm) plus ~0.6 s. -The unit's -`ExecStartPost=-/usr/bin/podman exec frank-tts-engine /app/warm-voices.sh` does -the same at every boot, so a fresh boot is already warm; the explicit call above -is for **adding one voice to a running engine** and needs no restart — a restart -drops every registration and re-pays the cold cost for the whole library. The -`podman exec` form is not cosmetic: the engine **publishes no port**, so there is -no host address to curl. +## Ops notes -## Proof (do not report "added" without it) - -```bash -curl -s localhost:7772/v1/audio/voices # name is listed -time curl -s -o /tmp/a.wav localhost:7772/v1/audio/speech \ - -H 'content-type: application/json' \ - -d '{"model":"tts-1","voice":"NAME","input":"Checking the cloned voice.","response_format":"wav"}' -time curl -s -o /tmp/b.wav localhost:7772/v1/audio/speech \ - -H 'content-type: application/json' \ - -d '{"model":"tts-1","voice":"NAME","input":"And the second request.","response_format":"wav"}' -file /tmp/a.wav /tmp/b.wav # really audio -``` - -The warmer's journal lines carry `voice NAME cold=…ms warm=…ms` — quote that pair. -Note that streaming and buffered output differ at the same seed (92.2 % of samples, --41.6 dB RMS): never compare two paths or two "identical" renders, only that each -is audio and that the second request is fast. - -## Failure modes worth naming - -- `400 unknown voice …; available voices: …` → the name is not a pack: wrong - spelling/case, missing `reference.wav`, or `/home/dank/voices` root-owned/empty. - Trust the 400's list over any manifest. -- 400 with `(none: no voice pack with a reference.wav in /voices)` → mount problem, - not a voice problem. -- First request slow **after** a boot → the warm pass failed; read the unit's - journal (the warmer exits 0 and logs when there is no voice directory, so - silence there is not success either). -- Endpoint answers but synthesis fails 502 → the façade must dial - `http://frank-tts-engine:8880` over the shared bridge - (`frank-tts-engine.network`); a `host.containers.internal` / `127.0.0.1:8880` - upstream can never work between containers, because netavark DNATs published - ports only. - -## Never - -Never write under `/voices` from the endpoint (mounted `:ro` by design), never -`git add` a voice pack, never `rm` a pack (repo policy: move to `.AGENTTRASH/`), -and never benchmark against this endpoint while it serves — stop and mask the -units first. +- `systemctl --user stop` **removes** the quadlet container — `podman start` then + finds nothing. To run the engine without its `ExecStartPost` hook, use a + separate container, not stop+start. +- `podman cp` of a fixed script into a running container is a **trial only**: the + layer dies with the container. Merge + redeploy to make it durable. +- Nothing on the voice pair exposes `/metrics`; Prometheus can't scrape it (a + blackbox HTTP probe on 7772 is the only honest option). diff --git a/agent/managed-skills/gitea-api-pr-operations/SKILL.md b/agent/managed-skills/gitea-api-pr-operations/SKILL.md index 46fdfb5..754bbfa 100644 --- a/agent/managed-skills/gitea-api-pr-operations/SKILL.md +++ b/agent/managed-skills/gitea-api-pr-operations/SKILL.md @@ -1,105 +1,31 @@ --- name: gitea-api-pr-operations -description: "Open, list, merge, and verify pull requests (and diagnose issue-API scope failures) on the Dankclaw Gitea via its REST API from a workstation, keeping the token out of argv (curl -K config file, mode 600) — includes the credential-store extraction one-liner, the merge endpoint's Do-field law and Content-Type-header trap, the suggested-URL-is-not-a-PR gotcha, and the insufficient-token-scope trap." +description: "Open, list, merge, and diagnose issues/PRs on the Dankclaw Gitea API (192.168.0.143:3000, dank/command-center) with curl + token — merge payload quirk included" --- -# Gitea API PR operations (Dankclaw) +# Gitea API PR operations on Dankclaw -`git push` on this fleet prints `Create a new pull request for ...` — that is a -**suggestion, not a PR**. Verify (`GET /pulls?state=open` → expect `[]`) before -claiming a PR "should exist"; create it via the API when asked. - -## Endpoint + auth -- API base: `http://192.168.0.143:3000/api/v1/repos/dank/` -- Token lives in the git credential store configured by - `git config --get credential.helper` → `store --file ~/.config/secrets/gitea`, - line format `http://user:TOKEN@192.168.0.143:3000`. -- The repo-local `.env` `GITEA_TOKEN` (main checkout, gitignored) carries - **`read:issue` too** (measured 2026-10-18): filing issues needs THAT token, - the credential-store one 403s on issues. - -Extract the token (the `http://` scheme contains a colon — scheme-stripping -awk regexes silently grab the port instead; use this anchored sed): +Verified 2026-10-18 on Gitea at `http://192.168.0.143:3000`, repo `dank/command-center` (owner `dank`). +## Token handling (never echo) ```bash tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1) -[ -n "$tok" ] && echo "token-extracted(len=${#tok})" # length check, never echo the value +cfg=$(mktemp) && chmod 600 "$cfg" && printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg" +# use: curl -sS -K "$cfg" ... then rm -f "$cfg" ``` -## Keep the token out of argv (repo secret law) -`curl -H "Authorization: token $tok"` is world-readable in `/proc//cmdline`. -Use a mode-600 config file: +## Endpoints that work +- Open PR: `POST /api/v1/repos/dank/command-center/pulls` with JSON `{title, head, base, body}` (build with `jq -n --arg`; `head` = branch name as-is, slashes included). +- Patch PR (title/body): `PATCH .../pulls/` — works, returns 201 with full PR JSON (`mergeable` field handy). +- **Merge PR: `POST .../pulls//merge` with body `{"Do":"merge"}`** — capital-D `Do` is this Gitea's field (mirrors web UI form). `{}` or `{"DoNotMerge":false,...}` → 422 `[Do]: Required`. Success may return EMPTY body — verify with a separate `GET .../pulls/` for `{state:"closed", merged:true, merge_commit_sha}`. +- Issues: `POST .../issues` `{title, body}`; patch `PATCH .../issues/`. +- Issue/PR bodies MUST end with the AGENTS.md footer: `---` line, then `cwd: · session: `. Embed the footer inside the body string (a bare `---` as a separate shell arg breaks `jq` arg parsing). +- `xdg-open ` lands the PR/issue in Frank's Firefox — do it the moment a PR opens. -```bash -cfg=$(mktemp); chmod 600 "$cfg" -printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg" -``` +## Gotchas +- POST/PATCH need `-H 'Content-Type: application/json'` on the curl command line; `-d @file` alone with a wrong content type yields 422 `"model.assignUsers: Bind is required"`. +- Merge-with-empty-response is normal; never conclude failure from blank stdout — GET the PR. +- No `tea` CLI anywhere; API + curl is the only path. GITEA_TOKEN env exists in `~/.omp/.env` (gitignored) as alternative source. -- POST JSON needs a second header line: `header = "Content-Type: application/json"`. - There IS NO `content-type` config option — curl rejects the file. -- `rm -f "$cfg"` when done (a mode-600 token file left in /tmp is its own leak). - -## Operations -```bash -# list open PRs -curl -sS -K "$cfg" ".../pulls?state=open" | jq -r '.[] | [.number,.head.name,.base.name,.title] | @tsv' -# create (build the payload as a FILE, see below) -curl -sS -K "$cfg" -o /tmp/pr.json -w '%{http_code}\n' -X POST -d @/tmp/pr-payload.json ".../pulls" -# verify: 201 is not enough — check mergeable -curl -sS -K "$cfg" ".../pulls/1" | jq '{number,state,merged,mergeable,base:.base.ref,head:.head.ref,url:.html_url}' -``` - -## Merging: the field is `Do`, and it is a STRING naming the strategy - -Measured 2026-10-18 on Gitea at 192.168.0.143:3000 (`POST .../pulls//merge`). -`MergePullRequestOption` is `{From, To, When, Do}` — **there is no `DoMerge` and -no `merge_method`**. Every wrong shape is a 422 with a distinct message, so read it: - -|body sent|http|message| -|---|---|---| -|`{"DoMerge":true,"merge_method":"merge"}`|422|`[Do]: Required`| -|`{"Do":true,"merge_method":"merge"}`|422|`[]: json: cannot unmarshal JSON boolean into Go string within "/Do"`| -|`{"Do":"true"}`|422|`[Do]: In…` (validation: not a strategy name)| -|form body `Do=true&merge_method=merge`|422|`[Do]: Required` (the value must be a strategy name)| -|`{"Do":"merge"}`|**200**|merged| - -```bash -curl -sS -K "$cfg" -H 'Content-Type: application/json' -X POST -d '{"Do":"merge"}' ".../pulls/4/merge" -# then prove it: .merged true, .state closed, .merge_commit_sha set, and origin/main moved -curl -sS ".../pulls/4" | jq -c '{number,state,merged,merge_commit_sha}' -``` - -Strategy names: `merge`, `rebase`, `rebase-merge`, `squash`, `manually` — a -strategy the repo does not allow is rejected too. - -**Merge trap (measured 2026-10-18):** on `POST /merge` the `Content-Type` header -must go on the **command line** (`-H 'Content-Type: application/json'`). Same -curl that succeeds on `POST /pulls` with Content-Type supplied only via the `-K` -config file returns **405 with an EMPTY body** on `/merge`. A 405 there is this -trap (or a base-moved mergeability recheck — GET the PR, see `mergeable:true`, -then retry with `-H`), NOT a permission problem. - -## Building issue/PR bodies without quote carnage -Hand-written `jq -n --arg t "..." '{title:$t, body:"..."}'` one-liners with long -markdown bodies WILL break on a stray quote (and the broken quoting can swallow -the following commands). Build bodies as files (heredoc or write tool), then: -```bash -b=$(jq -Rs . body.md) -jq -n --arg t "$title" --argjson b "$b" '{title:$t, body:$b, head:"", base:"main"}' > payload.json -curl ... -d @payload.json -``` - -## Gotchas measured 2026-10-14 / 2026-10-17 / 2026-10-18 -- Unauthenticated/bad token → HTTP 401 body `{"message":"not found"}`; `jq '.[]|[.number,..]'` then fails with *cannot index "not found"* — that is an auth failure, not a missing repo. -- **Insufficient token scope ≠ missing data.** The credential-store token - (measured 2026-10-17) carries only `write:repository,write:user`; the issues - API answers HTTP 403 `{"message":"token does not have at least one of required - scope(s), required=[read:issue], token scope=write:repository,write:user"}`. - jq surfaces that JSON as the *cannot index* error — do NOT conclude the issue - list is empty, and note POSTs under this token create NOTHING (verify, don't - assume retry safety). Filing issues needs a fresh token minted in the Gitea - web UI with `read:issue`+`write:issue` (Frank-class credential action; ask) — - OR use the main-checkout `.env` `GITEA_TOKEN`, which already has issue scope. - PR endpoints work fine with either token (`pulls` POST/GET/merge all 200/201). -- On the created-PR response `.head.name` can be null; verify with the `GET /pulls/` and use `.head.ref`. -- **Swagger JSON is disabled on this instance** — `/api/swagger.v1.json`, `/api/swagger.json` and `/api/v1/swagger.json` all 404. Do not go looking for the schema there; the 422 messages name the field and its Go type, which is the faster instrument. +## Post-merge propagation +The `~/.omp` checkout IS the deployment: `git -C ~/.omp fetch && git merge --ff-only origin/main` (dirty runtime files don't block ff). Never merge a PR at all without Frank's explicit OK. diff --git a/agent/managed-skills/niri-config-edit-validate/SKILL.md b/agent/managed-skills/niri-config-edit-validate/SKILL.md index 638a940..c0bfca0 100644 --- a/agent/managed-skills/niri-config-edit-validate/SKILL.md +++ b/agent/managed-skills/niri-config-edit-validate/SKILL.md @@ -22,7 +22,7 @@ description: "Locate, edit, validate, and commit niri compositor config on Frank - Preset list order = cycle order for `Mod+R` (`switch-preset-column-width`); `Mod+Shift+R` cycles backwards. WITH custom preset lists the cycle contains ONLY the configured presets — no implicit full/maximize tail (verified: [0.25, 0.5] cycles period 2). Maximize = separate `Mod+F` `maximize-column`. Built-in defaults without the node: 1/3, 1/2, 2/3. - WITHOUT `preset-window-heights`, `switch-preset-window-height` FALLS BACK to the width presets — user sees width-fractions as heights; define the node explicitly to stop it. - Ground truth for node placement: the binary's embedded template — `grep -a -B4 -A8 "preset-column-widths {" $(which niri)`. -- Window outlines: base geometry (`width`, `off`) lives in `cfg/layout.kdl` `focus-ring`/`border`; COLORS effectively live in `noctalia.kdl` (see include-order law). `border { off }` + focus-ring active = the visible outline. +- Window outlines: base geometry (`width`, `off`) lives in `cfg/layout.kdl` `focus-ring`/`border`; COLORS effectively live in `noctalia.kdl` (see include-order law). `border { off }` + focus-ring active = the visible outline. Frank's look since 2026-10-18: amber-orange `#ff8c00`, width 4 (`active`+`urgent` in noctalia.kdl; tab-indicator/insert-hint/recent-windows stay red `#f43841` by choice). ## Workflow 1. Edit the live file under `~/.config/niri/cfg/` (symlinked = repo file; except `noctalia.kdl`, edit live then `cp` to mirror). diff --git a/agent/managed-skills/skills-cli-retroactive-store-wiring/SKILL.md b/agent/managed-skills/skills-cli-retroactive-store-wiring/SKILL.md new file mode 100644 index 0000000..08a86e9 --- /dev/null +++ b/agent/managed-skills/skills-cli-retroactive-store-wiring/SKILL.md @@ -0,0 +1,31 @@ +--- +name: skills-cli-retroactive-store-wiring +description: "Retroactively register hand-copied skills with the vercel-labs skills CLI (global store + lockfile), wire omp via the agents provider (no repo symlinks), and extract authored overrides without twins" +--- + +# Skills CLI retroactive store wiring (omp tiers) + +Retrofit provenance for hand-copied skills and wire them for omp on Dank workstations. Executed 2026-10-18 on blankpad; doctrine doc: `omp-docs/tools/vercel-labs-skills/skill-store-wiring.md` (repo). + +## Tier law (final design — do NOT reintroduce per-skill symlinks in the repo) +| Tier | Path | Owner | +|---|---|---| +| Store | `~/.agents/skills/` + lockfile `~/.agents/.skill-lock.json` (v3) | `skills` CLI | +| Authored | `/agent/skills/` real dirs | Frank, branch → PR | +| Managed | `~/.omp/agent/managed-skills/` | `manage_skill` only | + +omp has NO agent id in the skills CLI (79-agent registry in `dist/cli.mjs`; `-a pi` writes `~/.agents/skills` too — pi detector keys on `~/.pi/agent`). omp reads the store DIRECTLY via its `agents` provider (`skills.enableAgentsUser`, default true; check with `read cfg://skills`). Precedence: authored (native, 100) > skillshare > ... > agents provider > managed (5). Identical copies collapse by realpath; differing same-named copies survive namespaced `agents/` with a startup warning — so when authoring an override, REMOVE the store twin (`npx -y skills remove -g`; prunes lockfile too) or `skills update -g` resurrects a twin. + +## Procedure (retroactive registration) +1. List the repo/live skill dirs to register; identify true sources (per-skill — e.g. `find-skills` is `vercel-labs/skills`, not `mattpocock/skills`). +2. Drift check FIRST, per skill, before anything moves: `diff -rq / ~/.agents/skills/` after canonical install; abort-and-report on drift (byte-identical = clean cutover). +3. Canonical install (scratch dir with a package.json; global scope): `npx -y skills add / --all -g` (+ per extra source `npx -y skills add vercel-labs/skills --skill find-skills --agent universal -g`). Artifacts: store dirs + `~/.agents/.skill-lock.json`; verify `jq '.skills|length'`. +4. Authored extraction (only for skills the user will customize): copy dir into repo `agent/skills//`, add an "Authored override" note after frontmatter, then `npx -y skills remove -g` and confirm `jq '.skills|has("")' == false`. +5. Clear repo copies of store-managed skills (repo records authored overrides ONLY). Live cutover after merge: move old dirs to /tmp snapshot, `git merge --ff-only origin/main` in the live checkout. +6. `skills ls` from a repo containing `agent/skills` mislabels skills as Eve project skills (path collision) — attribution noise, ignore. +7. Fresh-box recipe (three commands, no wiring): add `--all -g`, add find-skills, `remove grilling -g`. + +## Gotchas +- Eve/Codex `agents/openai.yaml` files ride along from upstream; omp ignores them — prune from authored dirs (they get swept off disk elsewhere, leaving phantom ` D` in git status). +- `skills remove` prunes only global scope if `-g` given; check lockfile after. +- Mid-run doc readers (subagents) may see pre-cutover trees — send them a correction message, don't let stale advice ship in issue bodies. diff --git a/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/architecture.md b/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/architecture.md index a64cefa..3fbe13a 100644 --- a/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/architecture.md +++ b/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/architecture.md @@ -22,7 +22,7 @@ **Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated** (`.hgn` format) -- Runtime: halogen-flash-server 0.16.2 (digest-pinned `ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84…`) +- Runtime: halogen-flash-server 0.16.2 (digest-pinned `ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84…`) - Format: v2 `.hgn` (flash-server exclusive) - MTP head: INSIDE checkpoint (`HALOGEN_MTP_HEAD` unset) - Vision: ON (v2 tower loads beside checkpoint) @@ -62,7 +62,7 @@ Every deployed, working Podman container MUST have a self-healing, boot-persiste Unit requirements: -- **Boot persistence** — `[Install] WantedBy=default.target`, installed to `~/.config/containers/systemd/.container`, enabled in the user manager, with linger on for the deploy user (`loginctl show-user dank -p Linger` → `Linger=yes`). +- **Boot persistence** — `[Install] WantedBy=default.target`, installed to `~/.config/containers/systemd/.container`, enabled in the user manager, with linger on for the deploy user (`loginctl show-user dank -p Linger` → `Linger=yes`). - **Self-healing** — `Restart=always` (or `on-failure`) with a short `RestartSec`; quadlet health check where the service has one. - **Repo provenance** — the `.container` file lives in this repo (`//quadlet/`), is installed by the deployer, and is covered by the quadlet test seam. The host-side unit is generated state; the repo file is the source of truth. diff --git a/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/queue/01a14c93-8870-75d9-9900-9a731559feef/00mveljelz-vh49.json b/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/queue/01a14c93-8870-75d9-9900-9a731559feef/00mveljelz-vh49.json new file mode 100644 index 0000000..d78e538 --- /dev/null +++ b/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/queue/01a14c93-8870-75d9-9900-9a731559feef/00mveljelz-vh49.json @@ -0,0 +1 @@ +{"v":1,"kind":"product_decision","statement":"merge 15 and deploy","source":"contextual_resolution","evidence":"you can merge 15 and deploy","friction":{"corrective":false,"regression":false,"subtle":false},"sessionId":"01a14c93-8870-75d9-9900-9a731559feef","ts":1792374566327} \ No newline at end of file diff --git a/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/state.json b/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/state.json index fb012fe..f2e9791 100644 --- a/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/state.json +++ b/agent/memories/sharpshooter/wt-20261017-181502-6d1fc1f-1oq2osmoyw03k/state.json @@ -1,8 +1,8 @@ { "v": 1, - "lastConsolidatedAt": 1792370846394, + "lastConsolidatedAt": 1792374446411, "lastResult": { - "at": 1792370515419, + "at": 1792373460132, "sessions": 1, "deltas": 1, "model": "frank-zealot" diff --git a/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/product.md b/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/product.md index 9801d4a..74dcfda 100644 --- a/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/product.md +++ b/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/product.md @@ -1,2 +1,3 @@ ## top bar - Preserve top bar customizations; customizations lost or obliterated on subsequent runs +- Todo list updates as tasks change, not waiting for entire bill of tasks to finish \ No newline at end of file diff --git a/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/state.json b/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/state.json index 8725d35..e3995b7 100644 --- a/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/state.json +++ b/agent/memories/sharpshooter/wt-20261018-170616-7c8a99d-3bf6g32a51amg/state.json @@ -1,8 +1,8 @@ { "v": 1, - "lastConsolidatedAt": 1792370776395, + "lastConsolidatedAt": 1792374676416, "lastResult": { - "at": 1792370420509, + "at": 1792374324899, "sessions": 1, "deltas": 1, "model": "frank-zealot" diff --git a/dotfiles/noctalia/.local/state/noctalia/settings.toml b/dotfiles/noctalia/.local/state/noctalia/settings.toml index 2869baa..9e9181c 100644 --- a/dotfiles/noctalia/.local/state/noctalia/settings.toml +++ b/dotfiles/noctalia/.local/state/noctalia/settings.toml @@ -275,13 +275,13 @@ transition = [ "fade" ] interval_seconds = 1200 [wallpaper.default] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_1kdgpg.jpg" [wallpaper.last] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_1kdgpg.jpg" [wallpaper.monitors.DP-1] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_1kdgpg.jpg" [wallpaper.monitors.DP-2] path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg" @@ -290,7 +290,7 @@ transition = [ "fade" ] path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg" [wallpaper.monitors.eDP-1] - path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg" + path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_1kdgpg.jpg" [weather] unit = "imperial"