live: heartbeat sync 2026-10-18T19:52:41-07:00 (Refs #11)

This commit is contained in:
Frank
2026-10-18 19:52:41 -07:00
parent 66e3f220e7
commit 332067a516
13 changed files with 231 additions and 44 deletions
+5 -1
View File
@@ -13,7 +13,7 @@ modelRoles:
default: llamacpp (dankserver)/frank-archon:medium
smol: llamacpp (dankclaw-3090)/frank-zealot:medium
web: web/searxng
speech: local/kokoro
speech: frank-tts (dankclaw)/frank-tts
judge: openrouter/~typesafe/jev-latest:auto
dictation: local/parakeet-tdt-0.6b-v3
memory: llamacpp (dankclaw-3090)/frank-zealot:medium
@@ -206,6 +206,10 @@ speech:
enabled: true
mode: yield
enhanced: true
# Kokoro voice for SPEAK-ALOUD only — the enum is Kokoro's, and this block never
# reaches an HTTP request. The tts tool forwards `voice_id` and nothing else, so
# a frank-tts pack name here would break the vocalizer and still leave a
# synthesis call voice-less. Say `voice_id: "narrator1"` in the call.
voice: am_fenrir
secrets:
enabled: true
@@ -0,0 +1,30 @@
---
name: corpus-voice-skill-rewrite
description: "Rewrite a skill to emulate an author corpus's voice: full read, style-DNA distillation from writing-for-agents levers, positive-phrasing pass, sharpened completion criteria"
---
# Corpus-voice skill rewrite
Rewrite a skill so it reads as if a chosen author wrote it. Done for `grilling` against the `mattpocock/skills` corpus (2026-10-18, command-center PR #20); behaviour preserved, voice swapped.
## Where the corpus lives
Installed third-party skills: `~/.agents/skills/<name>/SKILL.md` (the skills-CLI global store; `~/.agents/.skill-lock.json` names sources). Whole mattpocock corpus ≈ 2,800 lines — small enough to read inline in ~4 `cat` batches via bash; do NOT delegate the reading, style lives in the exact prose.
Read the author's own meta-skill FIRST if one exists (`writing-for-agents` for mattpocock — it states his levers explicitly), then the corpus practicing them.
## Style DNA checklist (mattpocock; generalise per author)
- **Leading words**: compact pretrained concepts repeated as tokens, never sentences (_red_, _seam_, _fog of war_, _brief_, _frontier_). Define each once, reuse everywhere. Prefer words already in the author's other skills so the corpus shares one language.
- **Vocabulary-first**: bolded glossary terms, then used exactly; explicit "_Avoid_: component, service".
- **Positive phrasing**: prohibitions rewritten as target behaviours; keep a prohibition only when no positive form exists (hard guardrails).
- **Sharpened completion criteria**: fuzzy bounds ("understanding reached") → checkable + exhaustive ("frontier empty and nothing sits blocked", "one artefact per question, counted before sending").
- **Zero history**: no provenance, attribution, or absorbed-from lineage in the body.
- **No-op hunting**: delete harness defaults and anything the environment already confesses (formats, rendering, caps). Gotchas the environment CANNOT confess (exclusive dialogs, Esc-abort semantics) stay inline — they're the cache worth keeping.
## Procedure
1. Read entire corpus; tabulate levers with per-skill evidence before writing.
2. Rewrite preserving settled behaviour 1:1 — voice only; diff the two versions for semantic drift.
3. Audit pass: grep the new text for history words (extracted/absorbed/from/upstream), machine-specific constants, repo-specific paths.
4. Run repo checks, commit as `style(<scope>)`, note the lever→instance mapping in the PR comment so the reviewer can grade the emulation.
@@ -1,31 +1,64 @@
---
name: gitea-api-pr-operations
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"
description: "Open, list, merge, and diagnose issues/PRs on the Dankclaw Gitea API (192.168.0.143:3000, repo dank/command-center) with the token stored in ~/.config/secrets/gitea — incl. the required merge payload and where review comments actually live on 1.27.3."
---
# Gitea API PR operations on Dankclaw
# Gitea API PR operations (Dankclaw `dank/command-center`)
Verified 2026-10-18 on Gitea at `http://192.168.0.143:3000`, repo `dank/command-center` (owner `dank`).
## Where
- Token: `~/.config/secrets/gitea` (`http://user:TOKEN@192.168.0.143` single line).
Extract WITHOUT echoing:
`tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)`
- Build a 600-perm temp curl config and use `curl -K`:
`cfg=$(mktemp) && chmod 600 "$cfg" && printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"`
Delete `$cfg` after use. NEVER pass `-H "Authorization: token $tok"` (the literal
header lands in transcript + logs). NEVER echo the token.
- Base URL: `http://192.168.0.143:3000/api/v1/repos/dank/command-center`.
## Token handling (never echo)
```bash
tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)
cfg=$(mktemp) && chmod 600 "$cfg" && printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
# use: curl -sS -K "$cfg" ... then rm -f "$cfg"
```
## Recipes (all verified live 2026-10-18)
- **Open PR** (token only needed for private repos; this one reads publicly but
POST needs auth): `POST /pulls` JSON `{title, head, base:"main", body}`.
- **PATCH PR title/body**: `PATCH /pulls/<n>` with JSON body `{title}` and/or
`{body}`. Returns 201 with the full PR. Use `jq -n --arg` to build JSON; for
multi-line bodies write the text to a temp file and `--arg b "$(cat b.txt)"`.
(jq CLI: a bare `---` argument after the program is parsed as a flag → error;
fold footers into the --arg string instead.)
- **List open PRs**: `GET /pulls?state=open` → `jq -r '.[] | "#\(.number) \(.head.ref) \(.title)"'`.
- **Read PR**: `GET /pulls/<n>` (no token; public repo) → `jq -c '{state,merged,mergeable,head:.head.ref}'`.
- **MERGE**: `POST /pulls/<n>/merge` with `-H 'Content-Type: application/json'`
and body **`{"Do":"merge"}`** — this build REQUIRES the `Do` field:
`{}` or `{"DoNotMerge":false,...}` → 422; `{"Do":"merge"}` → 200 +
`{"state":"closed","merged":true,"merge_commit_sha":"..."}`. Verify with a
plain GET /pulls/<n> afterwards (merge endpoint's own stdout may not parse).
Merge is the `--no-ff` merge commit on main; then `git -C ~/.omp fetch && git
merge --ff-only origin/main` propagates the live deployment.
- **Issues**: same shape at `/issues` (`POST {title, body}`), `/issues/<n>/comments`
(`POST {body}`, `PATCH .../comments/<id>`) — both work with the temp-cfg auth.
## 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/<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.
## Review comments: the API lies on 1.27.3 (learned 2026-10-18)
- `GET /pulls/<n>/comments` (inline review comments) → **404 `not found` on every
route** (token or not, `?pull_request_id=` or not) even when a review with
comments exists.
- `GET /pulls/<n>/reviews` works (shows `state`, `comments_count`) but bodies are empty.
- **The comments are readable from the PR HTML**:
`curl -sS http://192.168.0.143:3000/dank/command-center/pulls/<n> -o /tmp/pr.html`
then extract `<div class="content comment-content">` blocks
(python: `re.finditer(r'comment-content', src)` → widen ±800 chars, strip tags).
Duplicates appear (suggested-change UI shows text twice); dedupe mentally.
- Per-conversation resolve API also 404s — flag resolutions in an issue comment instead.
## Body-footer law (AGENTS.md)
Every issue/PR body ends with `---` line then `cwd: <wd> · session: <id>`. Keep
it inside the --arg string (not a `---` CLI arg).
## 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-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.
- Content-Type: POSTs with `-d @file.json` MUST carry
`-H 'Content-Type: application/json'` on the command line. `-H` entries put in
the curl config file are ignored for this (header ordering quirk).
- `xdg-open <pr-url>` lands the PR in Frank's Firefox the moment it is opened —
law, not courtesy.
- `jq: cannot index "not found" with "user"` = endpoint 404'd into an error
object; check `-w '%{http_code}'` before blaming jq.
- The token in `.git/config`'s remote URL is the same secret; the credential
file (`~/.config/secrets/gitea`) is the one git actually consults (global
credential.helper) and the one to reuse for API calls.
@@ -0,0 +1,29 @@
---
name: gitea-pr-api-cycle
description: "Open/update/merge PRs and issues on the Dankclaw Gitea API (192.168.0.143:3000, dank/command-center) with the token-safe curl+jq sequence, including the {\"Do\":\"merge\"} merge payload and post-merge live-checkout fast-forward."
---
# Gitea PR/issue cycle — dank/command-center
Friction-earned 2026-10-18 across PRs #15–#20 and issue #18: three separate fumbles (jq trailer flag, 422 merge payload, silent -o/-w swallowing) codified once.
## Where
- API base: `http://192.168.0.143:3000/api/v1/repos/dank/command-center/{pulls,issues}`
- Token: never inline, never echoed. Extract from the git credential file into a 600-perm temp curl config:
`tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)`
`cfg=$(mktemp); chmod 600 "$cfg"; printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"` → `curl -K "$cfg"`; `rm -f "$cfg"` when done.
- Unauthenticated GET works for state reads (`GET /pulls/N` → `jq -c '{state,merged,merge_commit_sha}'`).
## Body construction (the two traps)
- Build JSON with `jq -n --arg t <title> --arg b "$(cat b.txt)" '{title:$t, head:<branch>, base:"main", body:$b}'`; write the body to a temp file first (`printf '%s' … > b.txt`). NEVER pass `---` as a bare jq argument — jq parses it as a flag (`unknown flag: ---`). The AGENTS.md attribution trailer (`---` + `cwd: … · session: …`) goes INSIDE the body string.
- Always capture status explicitly: `-o r.json -w '%{http_code}'` and print it; a bare `-o` hides failures (a 422 looked like success-with-no-output once).
## Merge (the 422 trap)
`POST /pulls/N/merge` requires body `{"Do":"merge"}` — `Do` is the Required field on this Gitea. `{}`, `{"DoNotMerge":false}`, `{"merge_style":…}` all return 422 (`"[Do]: Required"`). Success may return an EMPTY body — verify by GET afterwards (`state=closed, merged=true`), not by the POST response.
- Merge only with Frank's explicit per-PR OK (repo law).
- Post-merge the live deployment must follow: `git -C ~/.omp fetch -q origin && git -C ~/.omp merge --ff-only origin/main` (the checkout IS the deployment; branch is `main`).
## PR etiquette per AGENTS.md
- Immediately after opening: `xdg-open <PR-URL>` lands it in Frank's Firefox.
- Redesign mid-review: amend + `git push -f` the branch, then PATCH the PR body (`/pulls/N`, `{"body":…}`) so title/body match the net diff — verify `{number,state,mergeable}` in the response.
- Issue creation identical shape: `POST /issues` with `{title, body}`, expect 201; PATCH same endpoint `/issues/N` to fix bodies retroactively, then GET-readback verify.
@@ -0,0 +1,42 @@
---
name: gitea-review-comment-html-scrape
description: "Read inline review comments on the Dankclaw Gitea when pulls/N/comments 404s (Gitea 1.27.3): scrape comment-content blocks and diff anchors from the PR HTML page"
---
# Gitea review-comment extraction when the API 404s
On the Dankclaw Gitea (192.168.0.143:3000, Gitea 1.27.3) the review-comment API is dead for inline comments:
`/api/v1/repos/{owner}/{repo}/pulls/{N}/comments` returns 404 `not found` **even with a valid token**, and `pulls/comments?pull_id=` / `?pull_request_id=` variants 404 too. `/pulls/{N}/reviews` works but its bodies are empty for inline reviews (read `comments_count` for how many exist); `/issues/{N}/comments` shows only conversation comments, never inline ones.
## Procedure
1. Confirm the review exists and how many inline comments it holds:
`curl -sS http://192.168.0.143:3000/api/v1/repos/dank/command-center/pulls/{N}/reviews | jq -c '.[] | {id, state, comments_count}'`
2. Scrape the PR page instead (no token needed for a public repo):
`curl -sS http://192.168.0.143:3000/dank/command-center/pulls/{N} -o /tmp/pr.html`
3. Extract each comment's text — every inline comment renders a `comment-content` block; dedupe (each comment appears twice in the HTML):
```python
import re, html
src = open('/tmp/pr.html').read()
seen = set()
for m in re.finditer(r'comment-content', src):
t = html.unescape(re.sub(r'<[^>]+>', ' ', src[m.start()-100:m.start()+800]))
t = re.sub(r'\s+', ' ', t).strip()[:400]
if t not in seen:
seen.add(t); print('>>', t)
```
The comment text appears twice back-to-back inside each block ("text text") — read it once. Blocks start after `Copy Link|Copy Source`; ignore those chrome words.
4. **Locate the file/line anchor** (comment-content blocks carry no `data-path`/`data-line` attrs on this build): search the raw HTML for the comment string, walk *backward* ~4–7 KB, strip tags; the nearby `agent/...` path, `@@ -a,b +c,d @@` hunk header, and surrounding diff context lines identify the anchored file and the quoted source line.
## Replying / housekeeping
- Per-conversation resolve API does not exist on this build (404s) — reply to the round as a normal issue comment (`POST /repos/{o}/{r}/issues/{N}/comments`, token via `curl -K` temp cfg, `header = "Authorization: token …"`, 600 perms, `jq -n --arg b "$(cat b.txt)" '{body:$b}'`) listing which note got which fix commit.
- Edit a posted comment: `PATCH /repos/{o}/{r}/issues/{N}/comments/{id}` with `{"body": ...}` — use to fix your own typos rather than double-posting.
- Token source on this box: `~/.config/secrets/gitea` (grep the remote-URL pattern); never echo it.
## Why this works
The PR page is server-rendered with full inline comment bodies; Gitea's "Copy Source" UI element guarantees the raw text is present pre-markdown-render.
@@ -1,12 +1,14 @@
---
name: omp-models-yml-runner-api-placement
description: Add or edit a non-chat (tts/image/embedding) model row in ~/.omp/agent/models.yml without disabling every custom provider
description: "Add or edit a non-chat (tts/image/embedding) model row in ~/.omp/agent/models.yml, wire the speech role, and diagnose a failing speech device without blaming a healthy endpoint"
---
# Scope
`~/.omp/agent/models.yml` (or `models.yaml`) on a workstation: adding a **runner**
model row (TTS / image / embedding / rerank / video) alongside chat providers.
model row (TTS / image / embedding / rerank / video) alongside chat providers,
flipping `config.yml` `modelRoles` at it, and diagnosing a speech device that
refuses to speak.
# The landmine
@@ -63,9 +65,52 @@ yielding.
# Speech transport contract (`openai-speech`)
Body is `{model, input, response_format, voice?, speed?}`; `voice` is forwarded
only when explicitly set; `sample_rate`/`bit_rate` **throw**
**only** when the caller passed `voice_id`; `sample_rate`/`bit_rate` **throw**
(`openai-speech does not support sampleRate`). Codec comes from the
`output_path` suffix; 15 000-character cap. An unknown `voice` name is a server
error, not a fallback — and `auth: none` makes a dead endpoint read as
*available*, so point `modelRoles.speech` at such a provider only after it has
answered a real synthesis.
# Three speech-role laws (bundle-read + measured 2026-10-18)
1. **No setting carries a voice for `openai-speech`.** The transport builds the
body with `voice` from the call's `voice_id` and nothing else.
`tts.localVoice` is the *local Kokoro backend's* key; `speech.voice` is the
TUI's **speak-aloud** voice, an enum of Kokoro names that never reaches an
HTTP request (setting it to a pack name breaks the vocalizer and does nothing
for synthesis — read `cfgSpeechVoice` in `dist/cli.js` before believing
otherwise). So a voice-less call is the harness's **normal** shape: a
voice-required endpoint needs a server-side default (fleet answer:
`DEFAULT_VOICE` in `scripts/frank-tts.env`, journal marks the line
`(default)`, unknown names still 400 with the list).
2. **An explicit role has no fallback candidate.** `modelRoles.speech` set to one
model yields exactly one candidate, so a dead endpoint or an unresolvable
selector fails with *"No available speech model matches the speech role"*
instead of degrading to Kokoro. Loud-by-design; do not "fix" it by adding a
fallback in the client.
3. **`models.yml` loads at process start; `config.yml` is read live.** A session
older than the new row fails exactly like a dead endpoint while a fresh
process succeeds in the same minute.
# Diagnosing "speech device won't speak" — in this order
1. `curl` the endpoint directly with a real voice name. 200 + audio ⇒ the service
is fine and the fault is harness-side; stop touching the server.
2. Control test in the *same* session: point `modelRoles.speech` at
`local/kokoro` and call the device. It works ⇒ `config.yml` is live-read and
the runner selector or its voice argument is the problem, not the endpoint.
Revert the role immediately after.
3. Distinguish stale registry from dead endpoint with a fresh process:
`omp -p --no-session --no-extensions --no-skills --no-rules --tools tts --cwd /tmp
'Call the tts tool exactly once with text="…" output_path=/tmp/x.wav
voice_id=<name> — then reply with the tool result line.'`
Succeeds ⇒ the long-lived session predates the `models.yml` row; restart it.
4. Cross-check server-side, never trust the client's echo: the façade/access log
line and the engine's own ledger must carry the same byte count the tool
printed. (`model=…` in a tool result is what the client *requested*; a server
line is the fact.)
5. For harness-behaviour claims, read the bundle rather than reasoning from the
tool description: `/home/blank/.bun/install/global/node_modules/@oh-my-pi/pi-coding-agent/dist/cli.js`,
e.g. `grep -o -E '.{300}response_format:[a-zA-Z$]+.{250}'` for the speech body
builder, `grep -o -E 'cfgSpeechVoice:.{0,400}'` for what a settings key feeds.
@@ -1,4 +1,4 @@
{
"v": 1,
"lastConsolidatedAt": 1792359739617
"lastConsolidatedAt": 1792378334862
}
@@ -115,4 +115,4 @@ This rule exists because subagent hangs were observed in early deployments (2026
Before declaring a subagent deployed: verify the heartbeat endpoint returns within the timeout window, and confirm the recovery path is wired (retry/fallback/escalation logic exists in the codepath).
A subagent without a heartbeat or recovery path is a gap: wire the heartbeat, add the recovery path, then deploy.
A subagent without a heartbeat or recovery path is a gap: wire the heartbeat, add the recovery path, then deploy.
@@ -1 +0,0 @@
{"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,
"lastConsolidatedAt": 1792374446411,
"lastConsolidatedAt": 1792378149820,
"lastResult": {
"at": 1792373460132,
"at": 1792374778678,
"sessions": 1,
"deltas": 1,
"model": "frank-zealot"
@@ -1,6 +1,6 @@
{
"v": 1,
"lastConsolidatedAt": 1792374676416,
"lastConsolidatedAt": 1792378276529,
"lastResult": {
"at": 1792374324899,
"sessions": 1,
+13 -8
View File
@@ -269,14 +269,19 @@ providers:
# when explicitly set, and sampleRate/bitRate are REJECTED outright (the
# transport throws "openai-speech does not support sampleRate"), so the
# codec comes from the output_path suffix and the only tunable that matters
# is the voice name — which must be a real pack name (narrator1/2/3); an
# unknown name is an engine error, not a fallback.
# DELIBERATELY NOT WIRED YET: config.yml `modelRoles.speech` still points at
# local/kokoro. Flip it to "frank-tts (dankclaw)/frank-tts" only after the
# service is deployed and has answered a real synthesis, because `auth: none`
# makes the provider read as AVAILABLE even while the container is down — the
# role would then fail instead of falling back to Kokoro. The same flip must
# re-key `speech.voice` (now am_fenrir, a Kokoro voice) to a pack name.
# is the voice name — and it comes from ONE place: the tool's `voice_id`
# argument. No setting feeds it (`speech.voice` is the speak-aloud Kokoro enum,
# `tts.localVoice` is the local backend's), so a call that omits voice_id arrives
# at 7772 with no `voice` key and the endpoint answers 400 "unknown voice None;
# available voices: narrator1, narrator2, narrator3". Say the voice in the call.
# WIRED 2026-10-18: `config.yml` `modelRoles.speech` is this row, flipped only
# after the service was deployed and answered real synthesis (two packs,
# different bytes, 24 kHz PCM16, RTF 0.57). Why the gate existed: `auth: none`
# makes this provider read as AVAILABLE even while the container is down, and an
# explicit speech role has NO fallback candidate, so a dead 7772 fails the device
# loudly instead of quietly narrating in Kokoro. Reverting is ONE key:
# `modelRoles.speech: local/kokoro`. `speech.voice` stays a Kokoro name either
# way — it was never this endpoint's voice.
# SCHEMA LAW, learned the hard way (2026-10-18): provider-level `api` must be
# a CHAT transport — the models.yml schema accepts only the chat/judge APIs
# there, and naming a runner API (`openai-speech`) at provider level fails the