live: heartbeat sync 2026-10-18T18:51:41-07:00 (Refs #11)
This commit is contained in:
1 parent
7de07ee35c
commit
0ee08bc368
10 files changed
+199
-207
No files matched your search
@@ -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 <base> 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 <image> --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 <tag>`,
|
||||||
|
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.
|
||||||
@@ -1,125 +1,81 @@
|
|||||||
---
|
---
|
||||||
name: frank-tts-add-cloned-voice
|
name: frank-tts-add-cloned-voice
|
||||||
description: "Add or repair a cloned voice on the dank fleet: /home/dank/voices/<name> 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
|
# Adding (or repairing) a cloned voice on the dank fleet
|
||||||
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).
|
|
||||||
|
|
||||||
## 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
|
## Stack, fast
|
||||||
`POST /v1/audio/speech` (omp's TTS device may send `voice_id`, same value). Use:
|
|
||||||
|
|
||||||
- `^[a-z0-9][a-z0-9._-]*$`, **lowercase**, ≤ 64 chars. No slash, no `..`, no
|
- `frank-tts-engine` — arm A `qwentts.cpp tts-server`, container-internal `:8880`,
|
||||||
space, no uppercase; must not start with `.`/`_`/`-`.
|
publishes **no port**. Reach it with `podman exec frank-tts-engine curl …`.
|
||||||
- Why lowercase rather than just "alphanumeric": the shim accepts
|
- `frank-tts` — OpenAI façade on host `:7772`; resolves voice **by name**.
|
||||||
`[A-Za-z0-9][A-Za-z0-9._-]{0,63}` (`app.py`, `VOICE_NAME_RE`) but the **engine
|
- Packs: `/home/dank/voices/<name>/` with `reference.wav` (+ manifest, `.spk`,
|
||||||
lowercases names**, so `Narrator1` resolves on one side and not the other. The
|
`.rvq`). Builder: `scripts/build-voice-pack.sh` (crop 19.84–25 s, first 10 s of
|
||||||
boot warmer refuses a mixed-case pack loudly instead of half-working.
|
any source is forbidden).
|
||||||
- Names are user-visible: a 400 carries them in `available_voices`. Fleet
|
- Weights: `/home/dank/models/Qwen3-TTS-GGUF/` —
|
||||||
convention is `narrator1`, `narrator2`, … for the open-notebook podcast
|
`scripts/download-frank-tts-engine-qwentts.sh [--verify-only]` (1.4 GiB, ~18 s
|
||||||
speakers — open-notebook is configured with exactly `narrator1/2/3`, so renaming
|
on the LAN; verifies byte count + Hub LFS sha256).
|
||||||
those breaks podcast rendering, not just TTS — and a lowercase personal name for
|
|
||||||
a person's own voice.
|
|
||||||
|
|
||||||
## Pack layout (host state — never commit it)
|
## Sequence
|
||||||
|
|
||||||
`/home/dank/voices/<name>/` 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.
|
||||||
|
|
||||||
```
|
## Trap 1 — the kernel's argument cap (this broke the first deploy)
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
## Trap 2 — two readers of the env file disagree about quotes
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
Knobs live in `scripts/voice-packs.env` (one `_SOURCE`/`_OFFSET`/`_DURATION`/
|
`bash set -a; source` strips `"…"`. podman's `--env-file` (how the unit really
|
||||||
`_SHA256` set per voice). Three things it enforces that an operator gets wrong:
|
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
|
## Trap 3 — locally built images skip rebuilds on a stale hash
|
||||||
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/<voice>-<UTC>/`.
|
|
||||||
|
|
||||||
Files must be world-readable (the container runs as uid 10000): `644` files,
|
`build_compose_image` compares a `cfhash` label; it must cover every file the
|
||||||
`755` dirs. `mkdir -p /home/dank/voices` as the deploy user if it is absent —
|
`Containerfile` `COPY`s, or an edited `app.py`/hook deploys "successfully" while
|
||||||
rootless podman auto-creates a missing bind source **root-owned**, and the
|
the container runs old code.
|
||||||
endpoint then sees no voices at all.
|
|
||||||
|
|
||||||
## 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
|
| thing | value |
|
||||||
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.
|
| first synthesis of a freshly registered voice | 1 883 ms |
|
||||||
Re-registering does **not** warm — only synthesizing does. So:
|
| 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
|
The trial's `6.2–8.0 s cold → 0.5 ms warm` claim does **not** reproduce; warming
|
||||||
# registers every pack that has a reference.wav, then speaks the warm sentence once
|
is for registration (registry is memory-only, re-register ≠ warm) plus ~0.6 s.
|
||||||
podman exec frank-tts-engine /app/warm-voices.sh
|
|
||||||
systemctl --user --no-pager --full status frank-tts-engine.service | tail -20
|
|
||||||
```
|
|
||||||
|
|
||||||
The unit's
|
## Ops notes
|
||||||
`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.
|
|
||||||
|
|
||||||
## Proof (do not report "added" without it)
|
- `systemctl --user stop` **removes** the quadlet container — `podman start` then
|
||||||
|
finds nothing. To run the engine without its `ExecStartPost` hook, use a
|
||||||
```bash
|
separate container, not stop+start.
|
||||||
curl -s localhost:7772/v1/audio/voices # name is listed
|
- `podman cp` of a fixed script into a running container is a **trial only**: the
|
||||||
time curl -s -o /tmp/a.wav localhost:7772/v1/audio/speech \
|
layer dies with the container. Merge + redeploy to make it durable.
|
||||||
-H 'content-type: application/json' \
|
- Nothing on the voice pair exposes `/metrics`; Prometheus can't scrape it (a
|
||||||
-d '{"model":"tts-1","voice":"NAME","input":"Checking the cloned voice.","response_format":"wav"}'
|
blackbox HTTP probe on 7772 is the only honest option).
|
||||||
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.
|
|
||||||
@@ -1,105 +1,31 @@
|
|||||||
---
|
---
|
||||||
name: gitea-api-pr-operations
|
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
|
Verified 2026-10-18 on Gitea at `http://192.168.0.143:3000`, repo `dank/command-center` (owner `dank`).
|
||||||
**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/<repo>`
|
|
||||||
- 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):
|
|
||||||
|
|
||||||
|
## Token handling (never echo)
|
||||||
```bash
|
```bash
|
||||||
tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)
|
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)
|
## Endpoints that work
|
||||||
`curl -H "Authorization: token $tok"` is world-readable in `/proc/<pid>/cmdline`.
|
- 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).
|
||||||
Use a mode-600 config file:
|
- Patch PR (title/body): `PATCH .../pulls/<N>` — works, returns 201 with full PR JSON (`mergeable` field handy).
|
||||||
|
- **Merge PR: `POST .../pulls/<N>/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/<N>` for `{state:"closed", merged:true, merge_commit_sha}`.
|
||||||
|
- Issues: `POST .../issues` `{title, body}`; patch `PATCH .../issues/<N>`.
|
||||||
|
- Issue/PR bodies MUST end with the AGENTS.md footer: `---` line, then `cwd: <wd> · session: <id>`. Embed the footer inside the body string (a bare `---` as a separate shell arg breaks `jq` arg parsing).
|
||||||
|
- `xdg-open <url>` lands the PR/issue in Frank's Firefox — do it the moment a PR opens.
|
||||||
|
|
||||||
```bash
|
## Gotchas
|
||||||
cfg=$(mktemp); chmod 600 "$cfg"
|
- 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"`.
|
||||||
printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
|
- 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"`.
|
## Post-merge propagation
|
||||||
There IS NO `content-type` config option — curl rejects the file.
|
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.
|
||||||
- `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/<n>/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:"<branch>", 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/<n>` 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.
|
|
||||||
@@ -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.
|
- 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.
|
- 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)`.
|
- 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
|
## Workflow
|
||||||
1. Edit the live file under `~/.config/niri/cfg/` (symlinked = repo file; except `noctalia.kdl`, edit live then `cp` to mirror).
|
1. Edit the live file under `~/.config/niri/cfg/` (symlinked = repo file; except `noctalia.kdl`, edit live then `cp` to mirror).
|
||||||
|
|||||||
@@ -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 | `<repo>/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/<name>` with a startup warning — so when authoring an override, REMOVE the store twin (`npx -y skills remove <name> -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 <live>/<name> ~/.agents/skills/<name>` 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 <owner>/<repo> --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/<name>/`, add an "Authored override" note after frontmatter, then `npx -y skills remove <name> -g` and confirm `jq '.skills|has("<name>")' == 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.
|
||||||
+1
@@ -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}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"v": 1,
|
"v": 1,
|
||||||
"lastConsolidatedAt": 1792370846394,
|
"lastConsolidatedAt": 1792374446411,
|
||||||
"lastResult": {
|
"lastResult": {
|
||||||
"at": 1792370515419,
|
"at": 1792373460132,
|
||||||
"sessions": 1,
|
"sessions": 1,
|
||||||
"deltas": 1,
|
"deltas": 1,
|
||||||
"model": "frank-zealot"
|
"model": "frank-zealot"
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
## top bar
|
## top bar
|
||||||
- Preserve top bar customizations; customizations lost or obliterated on subsequent runs
|
- 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
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"v": 1,
|
"v": 1,
|
||||||
"lastConsolidatedAt": 1792370776395,
|
"lastConsolidatedAt": 1792374676416,
|
||||||
"lastResult": {
|
"lastResult": {
|
||||||
"at": 1792370420509,
|
"at": 1792374324899,
|
||||||
"sessions": 1,
|
"sessions": 1,
|
||||||
"deltas": 1,
|
"deltas": 1,
|
||||||
"model": "frank-zealot"
|
"model": "frank-zealot"
|
||||||
|
|||||||
@@ -275,13 +275,13 @@ transition = [ "fade" ]
|
|||||||
interval_seconds = 1200
|
interval_seconds = 1200
|
||||||
|
|
||||||
[wallpaper.default]
|
[wallpaper.default]
|
||||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_73qev3.jpg"
|
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_1kdgpg.jpg"
|
||||||
|
|
||||||
[wallpaper.last]
|
[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]
|
[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]
|
[wallpaper.monitors.DP-2]
|
||||||
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
|
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"
|
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg"
|
||||||
|
|
||||||
[wallpaper.monitors.eDP-1]
|
[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]
|
[weather]
|
||||||
unit = "imperial"
|
unit = "imperial"
|
||||||
|
|||||||
Reference in new issue
Block a user