docs(prompts): 10-01 agent prompts — AS3, SPR, MP-22..MP-28, R-ACQ, RA-0, V3, V3b
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# AS3 — agent-sudo image build + AS3 runbook (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
You are a bounded background agent for agent-sudo (personal_projects #176). Budget: --max-turns 40.
|
||||
If you issue the same tool call twice with identical arguments, STOP and emit the wrap-up with
|
||||
status=partially_succeeded.
|
||||
|
||||
HARD RULES
|
||||
- NO sudo, ever. If a step truly needs root, end your run with a final message headed exactly
|
||||
`⏸ OWNER-COMMAND-REQUEST` containing host:, command:, why:, paste_back:, resume_at: — then stop.
|
||||
- NEVER restart/stop/recreate any container or systemd unit on either host (that is the owner's step, written
|
||||
into your runbook). `docker build`, `docker tag`, `docker push`, `docker run --rm` of the NEW image for a
|
||||
smoke test (no ports published, no bind mounts of the live bridge dir) are allowed on primary.
|
||||
- NO git add/commit/push. NO Postgres writes. Never print secret values (mask anything from .env files; do not
|
||||
cat .env files at all).
|
||||
- Do not make design decisions beyond the ones below; if reality contradicts them, STOP and report.
|
||||
|
||||
FACTS (verified by the main session 2026-10-01 17:10)
|
||||
- ASH hardening is LIVE on both hosts since 17:01 (primary) / 17:06 (server-01): root-owned daemon code at
|
||||
/usr/local/lib/agent-sudo/, signed SUDO.md copy at /etc/agent-sudo/SUDO.md (digest eba7b9c00856..), unit
|
||||
is a regular file with EnvironmentFile=/etc/agent-sudo/daemon.env. Daemon = AS1/AS2/ASH code.
|
||||
- Both containers still run the OLD image gitea.local/backtalk6858/agent-sudo:latest (b227d3cdfa9d, ~2 months
|
||||
old, pre-AS1 protocol). primary :8084 /health ok; server-01 bound 192.168.1.90:8082. A new container against
|
||||
the new daemon is required for tier 1–3 (old image speaks the pre-AS1 protocol).
|
||||
- Repo: /opt/appdata/docker/docker-compose/agent-sudo (uncommitted AS1/AS2/ASH changes; tests 185 pass).
|
||||
server-01's repo copy is OLDER (28+ commits behind) — do not touch it; the runbook syncs what the
|
||||
container needs.
|
||||
- server-01 Timeshift: /etc/timeshift/ has only default.json + restore-hooks.d (unconfigured). Target disk for
|
||||
rsync snapshots: /dev/sda4, ext4, UUID 6c2dbfca-8133-4e54-a510-8ffc19290f1c, mounted /data (931 G, 3 % used).
|
||||
SMART has never been read (needs root).
|
||||
- Signed SUDO.md (main-session grep 17:12): tier-1 ACTIVE rules are systemctl/cp/install rows only (e.g.
|
||||
`systemctl daemon-reload`, `systemctl enable --now control-plane-up.timer`, resolver timer rows); the
|
||||
`mkdir -p /opt/appdata/*` t1 row is NOT active. EVERY tier-3 row is `proposed` (none active). So §5 needs a
|
||||
SUDO.md change: in the runbook, write the exact proposed row to activate for the tier-3 test (narrowest
|
||||
possible, e.g. an exact `systemctl start as3-dry.service` row, tier 3, host_override primary=4) as a
|
||||
`SUDO.md.as3-proposed` file next to SUDO.md + the re-sign + install.sh + restart steps (mirror
|
||||
deploy/hardening/HARDENING_RUNBOOK.md §1–§3). Pick the §4 tier-1 test from the ACTIVE rows only.
|
||||
- Owner decision (09-30): build the AS3 image directly on primary, NOT via Jenkins (JH-3 not built yet).
|
||||
|
||||
TASKS
|
||||
1. Run the full test suite (python3 -m pytest -q -p no:cacheprovider in the repo). Must be 185 passed; else STOP.
|
||||
2. Read Dockerfile + app.py + docker-compose.yml. Confirm the image needs nothing that only the daemon has now
|
||||
(AS1 moved undo/snapshot/sandbox into the daemon). List what the image copies; do NOT edit the Dockerfile
|
||||
unless the build fails — if it fails, STOP and report the error.
|
||||
3. Build on primary: tag `gitea.local/backtalk6858/agent-sudo:as3-<short git sha of HEAD>-<yyyymmdd>` AND
|
||||
`:latest`. Smoke test: `docker run --rm --entrypoint python3 <new tag> -c "import app"` (or the equivalent
|
||||
import check for the real entrypoint) — no network, no mounts. Push both tags to Gitea (docker login is
|
||||
already configured for gitea.local; if push is refused for auth, STOP and report — do not handle creds).
|
||||
Record the image ID and the pushed digest.
|
||||
4. WRITE /opt/appdata/docker/docker-compose/agent-sudo/deploy/AS3_RUNBOOK.md — owner-facing, one command per
|
||||
block, each with Why/Expect, FULL commands (never abbreviate), in this order:
|
||||
§0 SMART read on server-01 (`smartctl -a /dev/sda` and the NVMe) — pass/fail criteria stated
|
||||
(reallocated/pending sectors, CRC errors); STOP the runbook if it fails.
|
||||
§1 Configure Timeshift on server-01 for RSYNC mode to the sda4 UUID above, minimal schedule off
|
||||
(snapshots on demand only), exclude /data itself and /var/lib/docker; one manual test snapshot +
|
||||
`timeshift --list`; how to delete that test snapshot.
|
||||
§2 primary: pull + recreate the agent-sudo container with the new tag (the compose is plain docker compose
|
||||
with an --env-file; find the exact env-file path and the exact command the July deploy used from
|
||||
DEPLOY_RUNBOOK.md; do NOT read .env contents). Then health check + one tier-0 /exec test through the
|
||||
container (a SUDO.md t0 command, e.g. `systemctl status docker`), with the exact curl incl. how the
|
||||
caller key is supplied WITHOUT echoing it (read -rs).
|
||||
§3 server-01: what the container needs from the repo (bridge/SUDO.md copy for the container — note the daemon
|
||||
validates against /etc/agent-sudo/SUDO.md), pull + recreate with SNAPSHOT_ENABLED=true, health, tier-0
|
||||
test.
|
||||
§4 tier-1 test on server-01 (an active t1 rule from the signed SUDO.md — pick one that is harmless, e.g.
|
||||
`mkdir -p /opt/appdata/<throwaway>`), confirm undo captured, run the undo, confirm reverted.
|
||||
§5 tier-3 test on server-01 on a THROWAWAY unit: write a tiny `as3-dry.service` (oneshot, ExecStart=/bin/true)
|
||||
— installing it needs root, so it's an owner block — then a t3 `systemctl start as3-dry.service` through
|
||||
agent-sudo; confirm Timeshift snapshot created + undo recorded; then cleanup (remove unit, delete snapshot).
|
||||
§6 rollback per host (previous image b227d3cdfa9d re-tag + recreate).
|
||||
Every tier test must be checked against the signed SUDO.md for the rule it relies on — quote the row.
|
||||
If a needed t1/t3 rule is not ACTIVE in the signed SUDO.md, say so in the runbook and in your output
|
||||
(do NOT edit SUDO.md — that needs a re-sign).
|
||||
5. Verify your own artifacts exist (ls + grep the runbook sections).
|
||||
|
||||
Wrap-up: FINAL message = JSON only: status, project "agent-sudo", actions_taken[], actions_failed[],
|
||||
files_touched[], image_tags[], image_id, pushed_digest, tests_passed, runbook_path, rules_relied_on[]
|
||||
(pattern, tier, state), missing_rules[], unverified_claims[], next_step, notes.
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# SPR — secrets-proxy retirement DRY RUN (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
You are a bounded background agent (tracking row personal_projects #174). Budget: --max-turns 35. DRY RUN:
|
||||
inventory + runbook only; you remove/revoke NOTHING. Same-call-twice → STOP with a partial wrap-up.
|
||||
|
||||
OWNER DECISION (2026-09-29 grill-me, final — do not re-litigate): RETIRE secrets-proxy, option (c).
|
||||
Approved list: rm container + image; remove the Traefik router + secrets-proxy.reverseproxyserver.net; REVOKE its
|
||||
prod + sandbox AppRole secret-ids, PROXY_CALLERS keys (incl. Jenkins proxy-caller-key), its NTFY bot token
|
||||
(dry-run list first); KEEP api_business.proxy_executions read-only and the Postgres role secrets_proxy as
|
||||
NOLOGIN; Jenkins 24 standard jobs → JH-3 (out of scope here, just list them); security hook → secret-exec skill
|
||||
(list where the hook references secrets-proxy); archive the Gitea repo.
|
||||
|
||||
FACTS (main session 2026-10-01 17:50): container secrets-proxy-secrets-proxy-1 exists, Exited (0) 2 months ago;
|
||||
image gitea.local/backtalk6858/secrets-proxy:latest (fd7270fe5f87) present on primary. Research:
|
||||
/opt/appdata/docker/research/secrets_proxy_S1_investigation.md (read it first).
|
||||
|
||||
HARD RULES
|
||||
- NO deletions, revocations, Vault writes, DB writes, container/image removal, Traefik edits, git, sudo.
|
||||
Read-only inspection only. `docker inspect` is fine; NEVER print env values (no `docker exec env`, no
|
||||
.env cat — list env KEY NAMES only via docker inspect + python that prints names). Vault: you may LIST paths
|
||||
and READ METADATA (AppRole role names, secret-id accessors via `auth/approle/role/<r>/secret-id` LIST) using the
|
||||
AppRole login pattern in /home/administrator/.claude/projects/-opt-appdata-docker/memory/playbook_vault_token_rotation.md
|
||||
Step 0 (login → use → revoke-self). If the policy refuses a list, record it as owner-needed, don't escalate.
|
||||
- Postgres read-only (resolve container: docker ps | grep '^postgres-'; one statement per -c).
|
||||
- Root needed? → final message headed `⏸ OWNER-COMMAND-REQUEST` (host, command, why, paste_back, resume_at).
|
||||
|
||||
TASKS
|
||||
1. Inventory every artifact on both hosts (primary 192.168.1.88, server-01 via ssh administrator@192.168.1.90):
|
||||
containers, images (all tags), compose dir, systemd units/timers, Traefik routers (dynamic files under
|
||||
/data/coolify/proxy/dynamic may be root-only — note it), Cloudflare DNS/tunnel entries referencing
|
||||
secrets-proxy (cloudflared config read-only), Vault roles/policies/paths/secret-id accessors, N8N workflows
|
||||
that call it (read-only API list if you can; else grep exports), Jenkins jobs referencing it, the security
|
||||
hook(s) in ~/.claude and /opt/appdata/docker/.claude that mention it, memory/playbook files that tell agents
|
||||
to use it, ntfy users/topics/tokens for it, Postgres roles/tables.
|
||||
2. For each: current state, the exact removal/revocation command (FULL, never abbreviated), who runs it
|
||||
(Claude vs owner/sudo), verification command, rollback note.
|
||||
3. Order the steps safely (revoke creds before removing the code that could re-use them? decide by dependency,
|
||||
explain), flag anything still referencing secrets-proxy that would BREAK on removal.
|
||||
|
||||
OUTPUT: /opt/appdata/docker/docker-compose/secrets-proxy/RETIREMENT_RUNBOOK.md (create the dir path's file only
|
||||
if the dir exists; else /opt/appdata/docker/research/secrets_proxy_retirement_runbook_2026-10-01.md).
|
||||
FINAL message = wrap-up JSON only: status, project "secrets-proxy", runbook_path, inventory[] (artifact, host,
|
||||
state, action, runner), breakage_risks[], owner_steps_count, claude_steps_count, actions_taken[],
|
||||
actions_failed[], unverified_claims[], next_step, notes.
|
||||
@@ -0,0 +1,133 @@
|
||||
# MP-22 — MP-10 enforce readiness (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
You are a bounded background agent for the media_pipeline project. Budget: --max-turns 45.
|
||||
If you issue the same tool call twice with identical arguments, STOP and output the wrap-up block with
|
||||
status=partially_succeeded. (Exception: a bounded WAIT — one ssh command that loops remotely with
|
||||
`timeout <=900 bash -c 'until <cond>; do sleep 30; done'` — may be repeated at most 3 times per test step.)
|
||||
|
||||
HARD RULES
|
||||
- NEVER restart, stop, recreate, or exec a mutating command in ANY live/prod container, on any host.
|
||||
Read-only `docker exec ... sha256sum|ffprobe|cat` and `docker ps/inspect` on live containers are allowed.
|
||||
- Container restart allowlist starts EMPTY. Before you start/stop/restart a SANDBOX container, add it to
|
||||
your allowlist and log "ELEVATED <name>" in actions_taken; verify it is healthy after, and list it in
|
||||
containers_restarted. Only `media-pipeline-sandbox-*` containers on server-01 may ever be elevated.
|
||||
- NO git add/commit/push. NO writes to the primary host's Postgres (read-only SELECTs only). The main
|
||||
session does all DB bookkeeping.
|
||||
- Issue every mutating command as its OWN tool call; verify with a SEPARATE call (never mutate+sleep+verify
|
||||
in one `sh -c`).
|
||||
- Mask secrets in all output. You should need NO sudo. If a step truly needs a privilege you lack, end your
|
||||
run with a final message headed exactly `⏸ OWNER-COMMAND-REQUEST` containing host:, command:, why:,
|
||||
paste_back:, resume_at: — then stop (max 3 per run). If a security hook/permission BLOCKS an action,
|
||||
do not work around it: emit a partial wrap-up.
|
||||
- Do not make design decisions. Every decision you need is pre-made in your phase block; if reality
|
||||
contradicts the block (code anchor missing, fixture absent, counts wrong), STOP and report.
|
||||
|
||||
ACCESS MAP
|
||||
- Prod DB (READ-ONLY): PG=$(docker ps --format '{{.Names}}' | grep '^postgres-'); then
|
||||
docker exec "$PG" psql -U postgres -d media_pipeline -c "<one SELECT>" (one statement per -c)
|
||||
- server-01: ssh administrator@192.168.1.90 (batch several read commands per ssh call).
|
||||
- Sandbox dir (server-01, and a git-tracked twin on the primary host at the same path):
|
||||
/opt/appdata/docker/docker-compose/server-01/media-pipeline-sandbox/
|
||||
- Sandbox containers: media-pipeline-sandbox-media-transcoder-sandbox-1, media-pipeline-sandbox-postgres-1
|
||||
(DB media_pipeline_sandbox, user sandbox, no password needed via docker exec). Pipe SQL files with
|
||||
docker exec -i media-pipeline-sandbox-postgres-1 psql -U sandbox -d media_pipeline_sandbox < <file>
|
||||
- Sandbox baseline reset: TRUNCATE pipeline_learning, transcode_jobs, transcode_overrides, pipeline_events,
|
||||
download_jobs, rename_jobs, show_configs RESTART IDENTITY; then pipe init/02-seed.sql. ALWAYS (re)start the sandbox transcoder (+ downloader-sandbox) AFTER a reset —
|
||||
TranscodeState caches the done-set at startup, so a file already done in the old DB is silently skipped.
|
||||
- sandbox-output/renamed/tmp hold ROOT-owned files; clear them from inside the container
|
||||
(docker exec media-pipeline-sandbox-media-transcoder-sandbox-1 find <dir> -mindepth 1 -delete), not host rm.
|
||||
- Fixture library: <sandbox>/sandbox-fixtures/<fix>/ (not scanned). STAGE = cp a clip into
|
||||
sandbox-input/<CATEGORY DIR>/<Show>/ ; category dirs: "H264 ENGLISH DUBBED ANIME TO BE TRANSCODED",
|
||||
"H264 ENGLISH SUBBED ANIME TO BE TRANSCODED", "H264 LIVE ACTION SERIES TO BE TRANSCODED",
|
||||
"H264 MOVIES TO BE TRANSCODED" (path drives preset + prefer_last — never flatten). sandbox-input/ must be
|
||||
EMPTY when you finish. Between runs clear sandbox-output/, sandbox-renamed/, sandbox-tmp/ contents.
|
||||
- The sandbox encodes with software libx265 (server-01 GPU is NVIDIA — no VAAPI). That is EXPECTED.
|
||||
- Script edit workflow: cp the LIVE /opt/appdata/docker/docker-compose/media-transcoder/media-transcoder.py
|
||||
to the primary-host sandbox dir as your working copy; record the live file's sha256 as BASELINE; edit the
|
||||
working copy; python3 -m py_compile it; scp it to the same path on server-01; restart the sandbox
|
||||
transcoder; confirm `docker exec media-pipeline-sandbox-media-transcoder-sandbox-1 sha256sum
|
||||
/app/media-transcoder.py` equals the working copy's sha256.
|
||||
- Promotion (only when your phase says PROMOTE and the verdict is GO): if the live file's sha256 still
|
||||
equals BASELINE, `cp <working copy> <live path>` (cp keeps the inode so the live bind mount sees it); then
|
||||
verify the host hash AND `docker exec media-transcoder-media_transcoder-1 sha256sum /app/media-transcoder.py`
|
||||
both equal the working copy. Do NOT restart live — put the human's restart command in your output.
|
||||
If BASELINE no longer matches: do not promote; report.
|
||||
- Log lines: the transcoder logs at INFO (no --debug in sandbox or prod); read them with
|
||||
ssh ... "docker logs --since <ts> media-pipeline-sandbox-media-transcoder-sandbox-1 2>&1 | grep -E '<pattern>'"
|
||||
|
||||
PERSIST BEFORE YOU FINISH
|
||||
- Append a dated block "## <PHASE> — <date> (background agent)" to
|
||||
/opt/appdata/docker/docker-compose/media-downloader-local/.claude/context.md with: What was done /
|
||||
Decisions / Current state / Next step (enough for a fresh agent to resume).
|
||||
- Run: python3 /opt/appdata/docker/.claude/scripts/embed_memory_dir.py --only-recent 5
|
||||
- Your FINAL message is the wrap-up JSON only, always (success or failure), containing at least:
|
||||
status (succeeded|partially_succeeded|failed), project ("media_pipeline"), actions_taken[],
|
||||
actions_failed[], files_touched[], containers_restarted[], unverified_claims[], next_step, notes
|
||||
— plus your phase's extra fields.
|
||||
|
||||
PHASE MP-22 — MP-10 enforce readiness: fix the duration source (S2 + output_is_complete), re-run the MP-10
|
||||
sandbox matrix in ENFORCE, decide GO/NO-GO for flipping prod to enforce. --max-turns 45. Tracking row:
|
||||
personal_projects #61. PROMOTE the code fix only on GO (see PROMOTION below). You do NOT flip prod to
|
||||
enforce — that is a compose env change + container recreate the owner does.
|
||||
|
||||
FACTS (verified by the main session 2026-10-01 16:40 — do not re-diagnose):
|
||||
- Live media-transcoder.py sha256 = 60a2f3ceb59532e310157022c7bbbc62e14d80a38eeae1b6ac5b93bc8fda1631 (= BASELINE;
|
||||
the repo file is identical and committed). VAAPI is live on prod (hevc_vaapi). Prod is in
|
||||
VERIFY_STREAMS_MODE=shadow (code default, line ~3094; not set in prod compose). 27 prod shadow verifies since the
|
||||
last container creation: ALL pass, 0 warn/fail.
|
||||
- BUG (found by MP-15 v2's Y1 run 09-25): `ffprobe_duration()` (line ~520) asks for `format=duration`; the
|
||||
`-select_streams v:0` there has no effect on format entries. A source whose UNMAPPED subtitle track has an event
|
||||
running past the end of the video gets an inflated format duration (Y1: src 154.63 vs out 150.15 → S2
|
||||
`duration_mismatch` false fail). Two consumers: `_v_duration()` (MP-10 S2, tolerance max(2 s, 0.5 %) + |audio
|
||||
delay|) via `sel["src_duration"]/["out_duration"]` (~line 3445), and `output_is_complete()` (MP-20b, ~line 3696,
|
||||
tolerance max(2 s, 1 %)) via `src_duration` at ~3766 and ~4927 — the latter can QUARANTINE a good output.
|
||||
|
||||
DECISIONS (pre-made by the main session; apply exactly):
|
||||
- D1 (the one feature): add `ffprobe_media_duration(path)` = the duration of the first VIDEO stream: ffprobe
|
||||
`-select_streams v:0 -show_entries stream=duration:stream_tags=DURATION` → use `stream.duration` if numeric,
|
||||
else parse the Matroska tag `DURATION` (HH:MM:SS.nnnnnnnnn), else fall back to `ffprobe_duration()` (format).
|
||||
Use it for BOTH the source and the output in S2 (~3445/3446) and for the source duration passed to
|
||||
`output_is_complete()` at both call sites, AND for the output side inside `output_is_complete()` (compare video
|
||||
to video). Leave every other `ffprobe_duration()` caller unchanged (list them in notes).
|
||||
- D2 (fixture): re-cut the D2 row (TrueHD veto) so its TrueHD track ENCODES. Previous attempt failed in the PARTS
|
||||
encode with ffmpeg's experimental truehd encoder. Try, in order, on server-01 inside the sandbox transcoder
|
||||
container (it has ffmpeg 6.1.2): (a) `-c:a truehd -strict -2 -ar 48000 -sample_fmt s16 -ac 2`; (b) same with
|
||||
`-ac 6`. Build from `sandbox-fixtures/mp10/MP10 D1 - S01E01.mkv` per the matrix row (a:0 truehd tagged `eng` no
|
||||
title, a:1 original tagged `jpn`). Save as `sandbox-fixtures/mp10/MP10 D2 - S01E02.mkv` (keep the old one as
|
||||
`MP10 D2 - S01E02.mkv.old-0924`). If neither encodes through the pipeline, mark D2 `covered_by_unit_test`
|
||||
(test_verify_output_streams.py must contain a truehd-veto test — check) and continue; that alone is NOT a NO-GO.
|
||||
- D3 (new fixture Y2 for the bug): cut a 150 s clip from `MP10 S2 - S02E02.mkv` (or the source used by Y1 if
|
||||
present under sandbox-fixtures/mp15*) and add a SECOND subtitle track (an SRT titled "English" with one event
|
||||
from 00:02:28 to 00:02:40, i.e. past the video end) that the picker will NOT map. Name it
|
||||
`MP10 Y2 - S02E04.mkv` under sandbox-fixtures/mp10/. Expected: OLD code (BASELINE) → S2 fail duration_mismatch;
|
||||
NEW code → S2 pass, output not quarantined.
|
||||
- D4: add unit tests for ffprobe_media_duration's three branches (stream duration / DURATION tag / format
|
||||
fallback) to test_verify_output_streams.py (mock run_subprocess). All existing sandbox tests must still pass
|
||||
(run every test_*.py in the primary-host sandbox dir with python3 -m pytest -q).
|
||||
|
||||
SANDBOX RUN (VERIFY_STREAMS_MODE=enforce for the sandbox transcoder only — set it via the sandbox compose
|
||||
environment; that file is the sandbox's own, you may edit it; revert to its previous value at the end):
|
||||
1. Baseline reset (COMMON). Run Y2 with BASELINE code in enforce → record the S2 false fail (proves the bug).
|
||||
2. Apply D1 to the working copy, py_compile, scp, restart, hash-confirm.
|
||||
3. Run the full §6 matrix from design/DESIGN_verification_hardening.md (D1–D5, S1–S3, L1 + L1 offset, M1 + M1
|
||||
offset) plus Y2, ONE fixture at a time (stage → wait → read `[VERIFY]` log line + transcode_jobs row → clear
|
||||
outputs → unstage). Record per row: expected verdict, actual verdict+codes, PASS/FAIL. Note: MP-15 v2 is NOT
|
||||
live (live has the original picker), so rows whose expectation depends on the picker (D4 sdh, L1) keep the
|
||||
§6 expectations as written.
|
||||
4. Enforce behaviour check on one failing row: status `failed_verification`, source kept, output quarantined and
|
||||
NOT in sandbox-renamed, no deletion_eligible_after.
|
||||
|
||||
GO criteria: Y2 fixed (old fails, new passes); every §6 row gets its expected verdict (D2 may be
|
||||
covered_by_unit_test); zero unexpected fails; all tests pass. Otherwise NO-GO with the exact failing rows.
|
||||
|
||||
PROMOTION (GO only): per COMMON (BASELINE guard, cp keeps inode, verify host + live container hashes). Also copy
|
||||
the updated test file(s) to /opt/appdata/docker/docker-compose/media-transcoder/ only if a copy already lives
|
||||
there; otherwise leave tests in the sandbox dir. Put in your output: (a) the owner restart command
|
||||
`cd /opt/appdata/docker/docker-compose/media-transcoder && docker compose restart media_transcoder`; (b) a
|
||||
STAGED (not applied) diff that adds `VERIFY_STREAMS_MODE: enforce` to the media_transcoder service environment in
|
||||
/opt/appdata/docker/docker-compose/media-transcoder/docker-compose.yml — write it to
|
||||
/opt/appdata/docker/docker-compose/media-transcoder/deploy/mp22_enforce.diff, do NOT edit the live compose.
|
||||
|
||||
Extra wrap-up fields: verdict (GO|NO-GO), matrix[] (row, expected, actual, pass), y2_old, y2_new,
|
||||
d2_outcome (encoded_a|encoded_b|covered_by_unit_test), tests_passed, working_copy_sha256, promoted (bool),
|
||||
other_ffprobe_duration_callers[].
|
||||
@@ -0,0 +1,68 @@
|
||||
# MP-23 — full media_pipeline code review + affected-files audit (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
You are a bounded background agent for media_pipeline (tracking row personal_projects #61). Budget: --max-turns 60.
|
||||
READ-ONLY REVIEW. If you issue the same tool call twice with identical arguments, STOP and emit the wrap-up
|
||||
with status=partially_succeeded.
|
||||
|
||||
WHY (owner directive 2026-10-01): MP-22 found that a 09-25 feature (MP-20b output_is_complete) shared a buggy
|
||||
helper (ffprobe_duration used format=duration, inflated by long subtitle tracks) with MP-10's S2 check. The
|
||||
symptom was seen on 09-25 but nobody checked the helper's other callers. The owner wants: (1) a review of the
|
||||
WHOLE project for existing bugs, (2) for every real bug, a check of already-processed files that it may have
|
||||
affected, listed as re-transcode candidates. Nothing gets fixed in this run — findings are discussed with the
|
||||
owner first.
|
||||
|
||||
HARD RULES
|
||||
- NO edits to any code, compose, config or DB. NO container start/stop/restart/exec-mutating. NO git. NO sudo
|
||||
(if you think you need it: stop with a final message headed `⏸ OWNER-COMMAND-REQUEST` — host, command, why,
|
||||
paste_back, resume_at).
|
||||
- Prod DB read-only: PG=$(docker ps --format '{{.Names}}' | grep '^postgres-'); docker exec "$PG" psql -U postgres
|
||||
-d media_pipeline -c "<one SELECT>" (one statement per -c). Never guess columns: \d <table> first.
|
||||
- Media files: read-only. There is no ffprobe on the host; use a throwaway container:
|
||||
docker run --rm -i -v /media/mediashare:/media/mediashare:ro --entrypoint sh gitea.local/backtalk6858/media-transcoder:latest -c '<ffprobe ...>'
|
||||
(sh, not bash). Keep probes bounded (sample, don't scan the whole library; max ~200 files per check).
|
||||
- Mask secrets; never print env/.env contents (`docker exec <c> env` is blocked by a hook — don't).
|
||||
- Write ONLY: your report file (below) and the context.md append. Scratch files in
|
||||
/tmp/claude-1000/-opt-appdata-docker-Machines-infrastructure-general-questions/771b2744-c468-4a6d-a4dc-a3886d0444b9/scratchpad/mp23/
|
||||
|
||||
SCOPE (live code; record each file's sha256 at start)
|
||||
- /opt/appdata/docker/docker-compose/media-transcoder/media-transcoder.py (live sha eca0129a…, ~5100 lines;
|
||||
highest priority)
|
||||
- /opt/appdata/docker/docker-compose/media-downloader-local/ (main .py)
|
||||
- media-api: find the live source (container media-api-nx9l470hzo695m2eg0d47jmy; `docker inspect` mounts/image to
|
||||
locate it; the sandbox twin is /opt/appdata/docker/docker-compose/server-01/media-pipeline-sandbox/media-api.py)
|
||||
- filebot monitor (file-monitor.py; locate the live copy the same way)
|
||||
- Design context (read, don't re-litigate): /opt/appdata/docker/docker-compose/media-transcoder/design/*.md and
|
||||
the media_pipeline section of /home/administrator/.claude/projects/-opt-appdata-docker/memory/playbook_media_pipeline_phases.md
|
||||
(fix inventory table + "Promotion lesson" notes). Known/open items there are NOT new findings — reference them.
|
||||
|
||||
METHOD
|
||||
1. Shared-helper sweep FIRST (the failure class that bit us): list every helper used by 2+ features (ffprobe
|
||||
wrappers, duration/offset/stream-index helpers, path/category helpers, DB writers, state/resume helpers). For
|
||||
each, list callers and check each caller's assumption against what the helper actually returns.
|
||||
2. Then a full read for: wrong-stream/wrong-index selection, unit mix-ups (s vs ms, samples), off-by-one in
|
||||
segment/part logic, exceptions swallowed into "success", status written before the work is verified, races
|
||||
between workers / the requeue peek / FileBot moves, path edge cases (spaces, quotes, brackets, unicode),
|
||||
resume state reused across code versions, DB writes that can leave inconsistent rows, deletion/cleanup that
|
||||
can remove a source or output early, timeouts that mark a job done.
|
||||
3. Every candidate bug must be CONFIRMED with evidence: exact file:line, the input that triggers it, and the
|
||||
wrong outcome. Prefer proving it with prod DB rows / logs (`docker logs media-transcoder-media_transcoder-1`
|
||||
read-only) or a probe of a real file. Unconfirmed suspicions go in a separate "suspected" list.
|
||||
4. Affected-files audit per CONFIRMED bug: query transcode_jobs (and rename_jobs to find where outputs went)
|
||||
for jobs that could have hit it since the buggy code went live (use git log on
|
||||
/opt/appdata/docker/docker-compose/media-transcoder/media-transcoder.py — read-only — for go-live dates),
|
||||
then verify a bounded sample (or all, if ≤ 50) with ffprobe. Produce the list of affected job ids + library
|
||||
paths as re-transcode CANDIDATES (do not queue anything).
|
||||
|
||||
OUTPUT: /opt/appdata/docker/docker-compose/media-transcoder/design/REVIEW_2026-10-01_full_code_review.md with:
|
||||
summary table (id, severity critical/high/medium/low, file:line, one-line bug, confirmed?, affected jobs count);
|
||||
per finding: trigger, evidence, impact, suggested fix (one paragraph, NOT applied), affected-files list;
|
||||
"suspected" list; testing-methodology gaps you observed (fixture coverage, shared-helper cross-check);
|
||||
files reviewed with sha256.
|
||||
Append a dated "## MP-23 — 2026-10-01 (background agent)" block to
|
||||
/opt/appdata/docker/docker-compose/media-downloader-local/.claude/context.md (what/decisions/state/next).
|
||||
Run: python3 /opt/appdata/docker/.claude/scripts/embed_memory_dir.py --only-recent 5
|
||||
|
||||
FINAL message = wrap-up JSON only: status, project "media_pipeline", report_path, files_reviewed[] (path,sha256),
|
||||
findings[] (id, severity, file_line, summary, confirmed, affected_job_ids[]), suspected[], retranscode_candidates[]
|
||||
(job_id, library_path, bug_id), methodology_gaps[], actions_taken[], actions_failed[], unverified_claims[],
|
||||
next_step, notes.
|
||||
@@ -0,0 +1,75 @@
|
||||
# MP-24 — file-monitor: never overwrite a library episode, never delete the only copy (FM-1 + FM-5) — test → GO/NO-GO (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
You are a bounded background agent for media_pipeline (tracking row personal_projects #61). Budget: --max-turns 60.
|
||||
Same tool call twice with identical arguments → STOP with a partial wrap-up.
|
||||
|
||||
CONTEXT (owner-approved 2026-10-01 18:09: fix order step 1 of the MP-23 review). Read first:
|
||||
- /opt/appdata/docker/docker-compose/media-transcoder/design/REVIEW_2026-10-01_full_code_review.md (FM-1, FM-5,
|
||||
plus FM-2/FM-4 for context only — NOT in scope)
|
||||
- Live code: /opt/appdata/docker/docker-compose/filebot-monitor/file-monitor.py (sha256 c0c1c46f… at review time —
|
||||
record it as BASELINE at start; container filebot-monitor-filebot-monitor-1 bind-mounts it :ro at /app/file-monitor.py).
|
||||
Main session verified FM-1 independently: rename_jobs 545 ('Yu Yu Hakusho 001.mkv') and 645 ('Yu Yu Hakusho 101.mkv')
|
||||
both landed on '.../Yu Yu Hakusho - S01E01 - …mkv' (04-06); 105 destination paths in rename_jobs have >1 distinct source.
|
||||
|
||||
DECISIONS (pre-made by the main session; apply exactly — ONE feature at a time, verify each before the next):
|
||||
- F1 (FM-1, no-overwrite guard): immediately before ANY move/rename into the library (every code path that ends in
|
||||
the library — find them all, incl. the season-1 remap paths at ~1819/1880 and _move_to_destination ~2276), if the
|
||||
destination file already exists:
|
||||
a) if it is the SAME content (same size AND same video duration within 1 s — use ffprobe as the code already
|
||||
does, or size+mtime if no probe helper exists; say which) → treat as already done (log, no overwrite, remove
|
||||
the incoming duplicate only AFTER confirming the library copy is intact);
|
||||
b) otherwise → NEVER overwrite. Move the incoming file to a conflicts dir
|
||||
`<Media to be Renamed root>/_conflicts/<show>/<original filename>` (create dirs; never overwrite inside it
|
||||
either — append `.1`, `.2`), record rename_jobs status `conflict` with error_msg naming both paths, and send
|
||||
ONE ntfy alert (existing notify helper, existing topic) "FileBot rename conflict: <dest> already holds a
|
||||
different file; incoming parked in _conflicts". No FileBot `--conflict override` anywhere: if the code passes
|
||||
`--conflict override` (or similar) to filebot, change it to `--conflict skip` and treat FileBot's skip result
|
||||
as case (b).
|
||||
- F2 (FM-5, no early delete): the finished transcode must exist in at least one of {its input location, the
|
||||
library destination, _conflicts} at EVERY point. Any rmtree/unlink of a job/temp dir that may still hold the only
|
||||
copy must first move any remaining video file back to the input location (or _conflicts if that path is now
|
||||
occupied). Concretely: after FileBot (which runs with --action move into staging), if any later step fails, move
|
||||
the staged file(s) back instead of deleting the staging dir. Never delete a source/staged file until the library
|
||||
destination is verified (exists, size matches).
|
||||
- Out of scope: FM-2 (double processing), FM-3/FM-4, DL-*, T-*. Do not fix them even if you see them; note any
|
||||
interaction.
|
||||
|
||||
TEST APPROACH (decided — the real tier-3 FileBot sandbox is deliberately not built: MP-12 owner decision, no
|
||||
docker.sock on server-01 because prod services run there):
|
||||
- Build a hermetic test: copy file-monitor.py to your working copy at
|
||||
/opt/appdata/docker/docker-compose/server-01/media-pipeline-sandbox/file-monitor.py (that path is the sandbox twin;
|
||||
it exists — overwrite it with the LIVE copy first, record its old sha256 in notes). Tests go in a new
|
||||
/opt/appdata/docker/docker-compose/server-01/media-pipeline-sandbox/test_mp24_filebot_guard.py using pytest +
|
||||
tmp_path dirs and a FAKE filebot: monkeypatch the function(s) that shell out to `docker exec filebot-cli filebot`
|
||||
so they "rename" deterministically (move file to a computed destination name) and can simulate: success, FileBot
|
||||
skip, FileBot failure, and a failure in a later step. Monkeypatch DB/ntfy/media-api calls to recorders.
|
||||
- Required scenarios (each must pass on NEW code; F1 cases 1–3 must FAIL on BASELINE to prove the bug):
|
||||
1. YYH case: library already has 'Show - S01E01 - X.mkv' (content A); incoming different file B maps to the same
|
||||
name → A untouched, B in _conflicts, rename_jobs status conflict, 1 ntfy.
|
||||
2. Same-content re-delivery → no overwrite, duplicate removed only after the library copy verified.
|
||||
3. Season-1 remap path collision (use the code's remap branch) → same as 1.
|
||||
4. FM-5: later step fails after FileBot moved the file → file back in input (or _conflicts), staging empty, no
|
||||
data loss. Must lose the file on BASELINE.
|
||||
5. Happy path unchanged: no existing destination → normal move, status done.
|
||||
6. Conflict dir name collision → `.1` suffix, nothing overwritten.
|
||||
- Also run the existing sandbox test files (python3 -m pytest -q test_*.py in that dir) — all must still pass.
|
||||
- Then a LIVE-SAFE dry check: python3 -m py_compile; import the module with its startup side effects disabled if the
|
||||
code allows (do NOT run the real main loop).
|
||||
|
||||
HARD RULES
|
||||
- NO edit to the LIVE file-monitor.py, NO container restarts (live or sandbox), NO git, NO Postgres writes (prod DB
|
||||
read-only SELECTs only), NO sudo (`⏸ OWNER-COMMAND-REQUEST` block if needed: host, command, why, paste_back,
|
||||
resume_at). Do not touch the media library or any real media file. Mask secrets; never dump env.
|
||||
- Do not make design decisions beyond F1/F2; if the code contradicts this block (e.g. no --action move, a different
|
||||
library-write path), STOP and report precisely.
|
||||
|
||||
PROMOTION: NOT in this run. On GO, the main session reviews the diff and promotes (cp keeps the inode; owner
|
||||
restarts filebot-monitor). Put in your output the exact promote + verify + restart commands with the BASELINE guard.
|
||||
|
||||
PERSIST: append "## MP-24 — 2026-10-01 (background agent)" to
|
||||
/opt/appdata/docker/docker-compose/media-downloader-local/.claude/context.md (what/decisions/state/next); run
|
||||
python3 /opt/appdata/docker/.claude/scripts/embed_memory_dir.py --only-recent 5.
|
||||
FINAL message = wrap-up JSON only: status, project "media_pipeline", verdict (GO|NO-GO), baseline_sha256,
|
||||
working_copy_sha256, diff_summary, library_write_paths_found[], scenarios[] (name, baseline_result, new_result,
|
||||
pass), tests_passed, promote_commands, interactions_with_out_of_scope_bugs[], actions_taken[], actions_failed[],
|
||||
unverified_claims[], next_step, notes.
|
||||
@@ -0,0 +1,64 @@
|
||||
# MP-25 — real FileBot sandbox on server-01 (filebot-cli-sandbox + exec-only socket gateway) (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
You are a bounded background agent for media_pipeline (tracking row personal_projects #61). Budget: --max-turns 50.
|
||||
Same tool call twice with identical arguments → STOP with a partial wrap-up.
|
||||
|
||||
GOAL (owner 2026-10-01 18:20): the sandbox filebot-monitor must run against a REAL FileBot, so file-monitor fixes
|
||||
(MP-24 now; FM-2/FM-4 later) are tested end to end. Build everything up to — NOT including — license activation;
|
||||
the main session activates with the owner's license file afterwards. Then resume-ready: once activated, the end-to-
|
||||
end test (section E) runs.
|
||||
|
||||
FACTS (main session, verified 18:20):
|
||||
- Live prod: container filebot-cli = rednoah/filebot:latest (FileBot 25.04, image id
|
||||
sha256:02fb2211c90ccb40e3cabfba7dfd94b57077aaf1c5f087d3da64c32c9b57c116), on PRIMARY. filebot-monitor execs
|
||||
`docker exec filebot-cli filebot ...` via docker.sock. Inspect both live containers READ-ONLY (docker inspect:
|
||||
mounts, env KEY NAMES only, user, entrypoint) to mirror the sandbox faithfully.
|
||||
- server-01 has NO filebot-cli container or image. It has gitea.local/backtalk6858/filebot-monitor:latest.
|
||||
- Sandbox: /opt/appdata/docker/docker-compose/server-01/media-pipeline-sandbox/ (git twin on primary, live copy on
|
||||
server-01). Service filebot-monitor-sandbox exists behind compose profile `mp14`; it sys.exit(1)s unless
|
||||
`docker exec filebot-cli filebot -version` works. Its working copy file-monitor.py = MP-24 build sha 057708dc…
|
||||
(do not modify it; if the container name `filebot-cli` is hardcoded, make the sandbox reach the sandbox FileBot
|
||||
WITHOUT editing the code — e.g. name the sandbox FileBot container so the exec target resolves, or an env var the
|
||||
code already reads; report which).
|
||||
- OWNER DECISION 2026-09-23 (MP-12): NO raw docker.sock in the sandbox — server-01 also runs prod (n8n-prod, hermes,
|
||||
jenkins); a socket would let the sandbox exec into those. Today's design (main session, keeps that intent): an
|
||||
EXEC-ONLY socket gateway that can reach ONLY the sandbox FileBot container.
|
||||
|
||||
DECISIONS (pre-made):
|
||||
- D1 FileBot: service `filebot-cli-sandbox` in the sandbox compose, image rednoah/filebot pinned by DIGEST to the same
|
||||
version as prod (25.04; resolve the digest of the live image id above — pull that exact digest on server-01; if
|
||||
the exact digest cannot be found on Docker Hub, use the newest 25.04-tagged digest and say so). Same mounts as prod
|
||||
but pointing at sandbox dirs (sandbox-renamed/…, sandbox-library/…, sandbox-filebot-staging). Its FileBot data dir
|
||||
(where the license is stored) on a named volume `filebot-sandbox-data` so activation survives recreates. Long-
|
||||
running (same command/entrypoint pattern as prod so `docker exec` works). No ports, sandbox network only.
|
||||
- D2 Gateway: wollomatic/socket-proxy (pin a digest; check its README for the current flags) with an allowlist of
|
||||
ONLY: GET /_ping, GET /version, GET /containers/<sandbox filebot container name>/json, POST
|
||||
/containers/<that name>/exec, POST /exec/[a-f0-9]+/start, GET /exec/[a-f0-9]+/json (+ resize if the docker CLI
|
||||
needs it). Everything else denied. Mounted docker.sock read-only on the gateway ONLY. filebot-monitor-sandbox gets
|
||||
DOCKER_HOST=tcp://<gateway>:<port> (no socket mount). Prove the restriction: from inside filebot-monitor-sandbox,
|
||||
`docker ps` → denied; `docker exec n8n-prod-h10eww4au274owpxozgizysh true` → denied; `docker exec <sandbox
|
||||
filebot> filebot -version` → works (license-free command). If the filebot-monitor image has no docker CLI, report
|
||||
how it execs (python docker SDK?) and adapt the allowlist accordingly.
|
||||
- D3 Keep everything behind the `mp14` profile (plain `docker compose up -d` must not start them).
|
||||
|
||||
HARD RULES
|
||||
- You MAY create/start/stop/recreate ONLY media-pipeline-sandbox-* containers + the two new sandbox services on
|
||||
server-01 (log "ELEVATED <name>"). NEVER touch prod containers on either host. NO git, NO Postgres writes on primary,
|
||||
NO sudo (`⏸ OWNER-COMMAND-REQUEST` block if needed). Never read or print the license file or any .env/secret value.
|
||||
- Edit the sandbox compose on BOTH copies (primary twin + server-01) identically; keep a .bak-mp25 of each first.
|
||||
- STOP before activation: the license is the main session's step.
|
||||
|
||||
E. END-TO-END (only after the main session resumes you saying "activated"): stage one fixture through the real
|
||||
FileBot via filebot-monitor-sandbox: (1) happy path → lands in sandbox-library; (2) collision: pre-place a
|
||||
different file at the target name → incoming lands in _conflicts, library untouched; record FileBot's exact
|
||||
stdout/exit code for the skip (verifies MP-24's regex); (3) clean up sandbox dirs. Use real-ish names the downloader
|
||||
won't skip (e.g. 'Yu Yu Hakusho - S01E01.mkv' from the mp6/mp10 fixture library).
|
||||
|
||||
PERSIST: append "## MP-25 — 2026-10-01 (background agent)" to
|
||||
/opt/appdata/docker/docker-compose/media-downloader-local/.claude/context.md; run
|
||||
python3 /opt/appdata/docker/.claude/scripts/embed_memory_dir.py --only-recent 5.
|
||||
FINAL message = wrap-up JSON only: status, project "media_pipeline", phase ("built_awaiting_activation" | "e2e_done"),
|
||||
filebot_image_digest, gateway_image_digest, gateway_allowlist[], restriction_proofs[] (command, result),
|
||||
exec_target_mechanism, license_activation_command (exact `docker exec ... filebot --license /path` with the path the
|
||||
main session should copy the .psm to on server-01), e2e[] (when run), filebot_skip_output (verbatim, when run),
|
||||
actions_taken[], actions_failed[], containers_elevated[], unverified_claims[], next_step, notes.
|
||||
@@ -0,0 +1,47 @@
|
||||
# MP-26 — FM-1 library restore PLAN (read-only) (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
Bounded background agent, media_pipeline, tracking row personal_projects #61. Budget: --max-turns 45. PLAN ONLY.
|
||||
Same tool call twice with identical arguments → STOP with a partial wrap-up.
|
||||
|
||||
CONTEXT: owner-approved fix order step 2 (2026-10-01): restore library files damaged by FM-1 (file-monitor overwrote
|
||||
existing library episodes). Read /opt/appdata/docker/docker-compose/media-transcoder/design/REVIEW_2026-10-01_full_code_review.md
|
||||
(FM-1 section + retranscode_candidates) and the MP-23 scratch evidence in
|
||||
/tmp/claude-1000/-opt-appdata-docker-Machines-infrastructure-general-questions/771b2744-c468-4a6d-a4dc-a3886d0444b9/scratchpad/mp23/
|
||||
(fm1_audit.txt etc., if present). Main session confirmed: rename_jobs 545 (YYH 001) and 645 (YYH 101) both → S01E01;
|
||||
105 destination paths in rename_jobs have >1 distinct source (most are harmless re-renames of the same content).
|
||||
The MP-24 no-overwrite guard is being promoted now (live soon); restores must not depend on the old behaviour.
|
||||
|
||||
TASKS
|
||||
1. For EVERY rename_jobs destination with >1 distinct source_path (all 105), classify: (H) harmless — same episode
|
||||
re-delivered (same show+episode, e.g. a re-transcode); (D) DAMAGE — a different episode/extra overwrote it. Use
|
||||
names + transcode_jobs rows + ffprobe of the current library file (duration, and compare against each candidate
|
||||
source's duration where the source still exists) to decide what the library file holds NOW. Probe via a
|
||||
throwaway container (no ffprobe on the host):
|
||||
docker run --rm -i -v /media/mediashare:/media/mediashare:ro --entrypoint sh gitea.local/backtalk6858/media-transcoder:latest -c '<ffprobe …>'
|
||||
2. For each D: (a) what the library file currently contains (episode identity), (b) which episode is MISSING from the
|
||||
library because of it, (c) whether the missing episode's original source still exists on disk (path), (d) whether
|
||||
the current content is itself a valid episode that belongs at ANOTHER library path (e.g. YYH 101 belongs at its
|
||||
correct season/episode — determine the correct SxxExx using the show's FileBot/TheMovieDB numbering as the library
|
||||
already uses for that show: check neighbouring files), and whether that other path is free or also damaged.
|
||||
3. Produce an ordered repair plan with exact actions, each labelled:
|
||||
- RENAME-IN-LIBRARY (move a wrongly-placed but valid file to its correct free path) — mv command,
|
||||
- RETRANSCODE (source on disk; recipe: source in its original input path with a newer mtime + output absent from
|
||||
'Media to be Renamed' → requeue peek re-transcodes; FileBot then places it — note the MP-24 guard will PARK it in
|
||||
_conflicts if the slot is still occupied, so order RENAME/park-aside steps first),
|
||||
- REDOWNLOAD (no source) — show + episode list for the owner,
|
||||
- LOST-EXTRA (OAD/recap/special overwritten; list only).
|
||||
Every command FULL (no abbreviations), with who runs it (files under /media are writable by administrator? check
|
||||
with ls -l + test -w on a sample, report), plus a verification step. Nothing destructive: a library file is
|
||||
never deleted — it is moved aside to '/media/mediashare/MY MEDIA/MY MEDIA/Media to be Renamed/_restore_hold/<show>/'
|
||||
first.
|
||||
HARD RULES: READ-ONLY. No mv/cp/rm/touch on any media file, no DB writes, no container changes, no git, no sudo
|
||||
(`⏸ OWNER-COMMAND-REQUEST` if needed). Bounded probes (≤ 300 files). Mask secrets.
|
||||
OUTPUT: /opt/appdata/docker/docker-compose/media-transcoder/design/RESTORE_PLAN_FM1_2026-10-01.md (summary table +
|
||||
ordered plan + REDOWNLOAD list + LOST-EXTRA list + method notes + per-entry evidence).
|
||||
Append "## MP-26 — 2026-10-01 (background agent)" to
|
||||
/opt/appdata/docker/docker-compose/media-downloader-local/.claude/context.md; run
|
||||
python3 /opt/appdata/docker/.claude/scripts/embed_memory_dir.py --only-recent 5.
|
||||
FINAL message = wrap-up JSON only: status, project "media_pipeline", plan_path, counts {harmless, damaged,
|
||||
rename_in_library, retranscode, redownload, lost_extra}, damaged[] (library_path, contains_now, missing_episode,
|
||||
action), writable_by_administrator (bool + evidence), actions_taken[], actions_failed[], unverified_claims[],
|
||||
next_step, notes.
|
||||
@@ -0,0 +1,41 @@
|
||||
# MP-27 — T2: environmental failures must not burn retries into skipped_permanent — sandbox → GO/NO-GO (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
Bounded background agent, media_pipeline (personal_projects #61). Budget: --max-turns 45. Owner-approved fix order
|
||||
step 3 (2026-10-01). Same tool call twice with identical arguments → STOP with a partial wrap-up.
|
||||
|
||||
READ FIRST: /opt/appdata/docker/docker-compose/media-transcoder/design/REVIEW_2026-10-01_full_code_review.md (T2,
|
||||
and T-S1/T-S2 for context) and the COMMON block in
|
||||
/home/administrator/.claude/projects/-opt-appdata-docker/memory/playbook_media_pipeline_phases.md
|
||||
("### COMMON block") — follow its HARD RULES + ACCESS MAP + script-edit workflow exactly (sandbox only; record
|
||||
BASELINE = live sha256 of /opt/appdata/docker/docker-compose/media-transcoder/media-transcoder.py, currently
|
||||
eca0129a…; scp the working copy to server-01 AND confirm the container hash — MP-24 lesson: the server-01 copy must
|
||||
match). Prod: 65 sources stranded as skipped_permanent (transcode_jobs ids 3350-3414; Blue Bloods S02/S05, Peaky
|
||||
Blinders S02, JJK S01 v2, JJK S03) after an environmental failure (read-only tmp / DNS) used all MAX_RETRY_FAILURES
|
||||
in ~3 minutes (media-transcoder.py ~3842-3843, 5001-5003, 5305-5315).
|
||||
|
||||
DECISIONS (pre-made — apply exactly, one feature):
|
||||
- D1 Classify failures: ENVIRONMENTAL = the failure is not about the input file (OSError EROFS/ENOSPC/EACCES on the
|
||||
tmp/output dirs, DNS/connection errors to media-api/DB, ffmpeg/ffprobe exit caused by a missing/unwritable output
|
||||
dir, subprocess timeout while the host is unhealthy — find the actual error strings the code sees; list them).
|
||||
INPUT = everything else (decode errors, verify failures, corrupt source).
|
||||
- D2 ENVIRONMENTAL failures do NOT increment the per-file failure count. Instead: exponential backoff for that file
|
||||
(start 5 min, cap 6 h), one ntfy alert per distinct environmental error class per 6 h (existing notify helper),
|
||||
and a preflight health check before each job (tmp + output dirs writable, media-api reachable) that pauses
|
||||
intake while unhealthy (log once per state change).
|
||||
- D3 skipped_permanent is NOT permanent across restarts for rows whose last error was environmental: on startup,
|
||||
those rows are eligible again (log "[REQUEUE-ENV] …"). INPUT failures keep today's MAX_RETRY_FAILURES semantics.
|
||||
- D4 No change to verification, encode, A/V, picker, or naming logic.
|
||||
TESTS: unit tests for the classifier (each env error string → ENVIRONMENTAL; a decode error → INPUT), backoff
|
||||
schedule, startup eligibility, and that INPUT failures still reach skipped_permanent after MAX_RETRY_FAILURES.
|
||||
Sandbox e2e: make sandbox-tmp read-only (chmod inside the sandbox transcoder container on the sandbox tmp mount, or
|
||||
the equivalent) → stage one fixture → expect backoff + no failure-count burn + 1 ntfy log line (ntfy disabled in
|
||||
sandbox: assert the log line) → restore writability → the job completes `done`. Then an INPUT-failure fixture
|
||||
(truncate a copy of a fixture to 1 MB) → reaches skipped_permanent after MAX_RETRY_FAILURES as today. Full sandbox
|
||||
test suite must pass. Clean sandbox at rest (COMMON rules).
|
||||
NOT IN SCOPE: requeueing the 65 prod rows (the main session does that after promotion), any other review finding.
|
||||
OUTPUT: wrap-up JSON only: status, project "media_pipeline", verdict (GO|NO-GO), baseline_sha256,
|
||||
working_copy_sha256 (primary twin AND server-01 container must match), env_error_strings[], tests_passed,
|
||||
e2e[] (case, result), promote_commands (BASELINE-guarded cp + verify + owner restart command),
|
||||
prod_requeue_plan (how to make the 65 rows eligible after promotion — SQL or restart — exact commands, NOT run),
|
||||
actions_taken[], actions_failed[], containers_restarted[], unverified_claims[], next_step, notes.
|
||||
PERSIST per COMMON (context.md block "## MP-27 — 2026-10-01 (background agent)" + embed).
|
||||
@@ -0,0 +1,39 @@
|
||||
# MP-28 — ffmpeg 6.1.2 segfaults decoding valid HEVC (Blue Bloods KONTRAST) → false verify failures — sandbox → GO/NO-GO (spawned 2026-10-01 23:15, main session 771b2744)
|
||||
|
||||
Bounded background agent, media_pipeline (#61). Budget: --max-turns 50. Same tool call twice → STOP (partial).
|
||||
Follow the COMMON block in /home/administrator/.claude/projects/-opt-appdata-docker/memory/playbook_media_pipeline_phases.md
|
||||
("### COMMON block": HARD RULES, ACCESS MAP, script-edit workflow incl. scp to server-01 + container hash check).
|
||||
|
||||
FACTS (main session, verified 23:10):
|
||||
- Live transcoder = 19b60afe… (MP-27). Image gitea.local/backtalk6858/media-transcoder:latest ships ffmpeg 6.1.2.
|
||||
- `ffmpeg -v error -i <Blue.Bloods.S02E01.1080p.WEBRip.x265-KONTRAST.mp4 SOURCE> -f null -` in that image →
|
||||
Segmentation fault, rc 139, deterministic. The same command with Jellyfin's ffmpeg 7.1.4
|
||||
(`docker run --rm --network none -v /media/mediashare:/media/mediashare:ro --entrypoint /usr/lib/jellyfin-ffmpeg/ffmpeg jellyfin/jellyfin:latest …`)
|
||||
→ rc 0, no errors. Source has a valid ftyp header and probes fine (2616.7 s).
|
||||
- These are HEVC video-COPY jobs: the only full video decode is verify_output_playback() (~line 3076), whose
|
||||
3-attempt loop treats SIGSEGV as transient and then returns a failure → MP-27 classifies INPUT → after 3 job
|
||||
attempts skipped_permanent + ntfy alerts. ~30 Blue Bloods S02/S05 sources affected (the MP-27 requeue set ids
|
||||
3350-3414 minus the 19 zero-headed JJK/Peaky files).
|
||||
DECIDE AND BUILD (decision rule pre-made; you pick by evidence):
|
||||
- Option A (preferred if it holds): upgrade ffmpeg in the transcoder image to a 7.x build (e.g. the jellyfin-ffmpeg
|
||||
7.1.x package or a static 7.1 build) — ONLY if, in the sandbox, the FULL MP regression set still passes (all
|
||||
existing test_*.py + the fixture matrices that past phases used: mp6, mp8, mp10 incl. Y2, mp18, mp20/mp21 HEVC
|
||||
copy, mp15 picker) with no output differences beyond trivial (compare verify verdicts, durations, stream maps,
|
||||
A/V av_align on 2 fixtures). VAAPI cannot be tested on server-01 (NVIDIA) → if A is chosen, list the exact prod
|
||||
VAAPI checks for the main session. Build the new image on PRIMARY? NO — image builds/pushes are the main session's;
|
||||
you produce the Dockerfile diff + build command, and test with a locally built image on server-01 ONLY if the build
|
||||
is small (<1 GB pulls) — otherwise test the binary by mounting it into the sandbox container.
|
||||
- Option B (fallback / or in addition): in verify_output_playback, a SIGSEGV is not proof of a bad file — re-verify
|
||||
with an independent check (e.g. second decoder = the same file through `-c:v copy -f null` packet walk + ffprobe
|
||||
`-count_packets` vs expected duration/fps, + audio-only full decode) and return a distinct verdict
|
||||
`verify_inconclusive_decoder_crash` that is NOT an INPUT failure (no skipped_permanent; one ntfy "decoder crashed
|
||||
on X, file kept, needs ffmpeg upgrade").
|
||||
- Recommend ONE (A, B, or A+B). Implement in the sandbox, prove with a REAL Blue Bloods source: copy ONE source
|
||||
(S02E01, ~890 MB) into the sandbox fixture library (main session pre-authorizes this read-only copy from the
|
||||
live share into sandbox-fixtures/mp28/ on server-01 via scp from primary), run it through the sandbox pipeline,
|
||||
expect `done` with verify pass (A) or inconclusive-not-permanent (B).
|
||||
OUTPUT: wrap-up JSON only: status, verdict (GO|NO-GO), recommendation (A|B|A+B) + why, ffmpeg_versions_tested,
|
||||
regression_results[], blue_bloods_e2e, dockerfile_diff (if A), build_push_commands (if A, for the main session),
|
||||
working_copy_sha256 (if B; twin = server-01 container), promote_commands, prod_vaapi_checks[], requeue_plan for the
|
||||
Blue Bloods rows (exact SQL/commands, NOT run), actions_taken[], actions_failed[], containers_restarted[],
|
||||
unverified_claims[], next_step, notes. PERSIST per COMMON ("## MP-28 — 2026-10-01 (background agent)" + embed).
|
||||
@@ -0,0 +1,46 @@
|
||||
# R-ACQ — research: automatic source re-acquisition for media_pipeline (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
Bounded RESEARCH agent. Budget: --max-turns 40. Read-only on the homelab; web research allowed (WebSearch/WebFetch).
|
||||
Same tool call twice with identical arguments → STOP with a partial wrap-up.
|
||||
|
||||
OWNER REQUEST (2026-10-01 21:17, paraphrased faithfully): when the pipeline needs a source file that no longer
|
||||
exists (e.g. a library file was damaged and must be re-transcoded), it should be able to download the source on its
|
||||
own. If autobrr grabbed it originally, autobrr (or its history) can re-grab it; but manually-downloaded sources won't
|
||||
have that, and the owner will soon start DELETING the saved .torrent files, so re-adding to qui/qBittorrent by hand
|
||||
won't work either. Needed: a way to search + fetch from the owner's sites by API/webhook/etc. If no API exists, the
|
||||
owner is open to building one (the API-business skills apply; it would be personal/non-commercial use).
|
||||
Owner's sites — now: nyaa.si and animebytes.tv (anime), eztv (TV), https://rarbg.torrentsbay.org/ (TV + movies).
|
||||
Near future: TorrentLeech, IPTorrents (TV + movies). Far future: PassThePopcorn (movies), BroadcasTheNet (TV).
|
||||
|
||||
QUESTIONS TO ANSWER (with sources/links for every claim; mark anything unverified):
|
||||
1. Per site: official API? (search + download .torrent/magnet), RSS, auth model (passkey/API key/cookie), rate limits,
|
||||
rules on automated access (private-tracker ToS: API/automation allowed? ratio/H&R implications of re-grabs),
|
||||
whether Prowlarr and/or Jackett have a maintained indexer definition for it (and its status), and for the
|
||||
rarbg.torrentsbay.org mirror: what it actually is (legitimacy, stability, malware risk) — flag clearly if it is a
|
||||
sketchy clone.
|
||||
2. Indexer managers: Prowlarr vs Jackett — Torznab API, how a custom service queries it (search by title +
|
||||
season/episode/absolute number, by IMDb/TVDB/AniDB ids), sync to Sonarr/Radarr, Docker images (linuxserver.io
|
||||
first per homelab rules), auth, resource use.
|
||||
3. Whole-stack option: Sonarr (TV/anime) + Radarr (movies) behind Prowlarr — they already implement "search for a
|
||||
missing/damaged episode and grab the best release" (incl. anime absolute numbering, release profiles). How would
|
||||
they coexist with the owner's existing pipeline (autobrr → qBittorrent/qui → media-downloader-local →
|
||||
media-transcoder → FileBot → Jellyfin)? Would Sonarr replace FileBot naming or sit beside it? What's the minimum
|
||||
integration where media_pipeline calls Prowlarr directly (search → pick → push to qBittorrent with a category the
|
||||
downloader already watches) without adopting Sonarr/Radarr?
|
||||
4. Release identity: what should the pipeline STORE at download time so an exact re-grab is possible later without
|
||||
the .torrent file (infohash, release name, indexer + guid, AniDB/TVDB ids)? Check the homelab DB:
|
||||
PG=$(docker ps --format '{{.Names}}' | grep '^postgres-'); docker exec "$PG" psql -U postgres -d media_pipeline -c "\d download_jobs"
|
||||
(and \dt) — read-only — and say what is already captured vs missing. Check what autobrr's own history/API exposes
|
||||
(container `docker ps | grep -i autobrr`, read-only inspect; do NOT read its secrets).
|
||||
5. Build-our-own option: only where no indexer definition/API exists — a minimal scraper/API (legal + ToS caveats,
|
||||
maintenance cost), and whether a Prowlarr custom (Cardigann YAML) definition is the cheaper "build our own".
|
||||
DELIVERABLE: /opt/appdata/docker/research/source_reacquisition_2026-10-01.md — per-site table (API, RSS,
|
||||
Prowlarr/Jackett support, automation allowed?, notes), the options compared (A: Prowlarr-only minimal integration;
|
||||
B: Prowlarr + Sonarr/Radarr; C: custom), a RECOMMENDATION with phased build steps, what to start storing now
|
||||
(schema additions), risks (private-tracker rules, ratio), and an UNVERIFIED list. Follow the 30-min gotcha-spike
|
||||
spirit: name the 3 most likely gotchas.
|
||||
HARD RULES: no installs, no container changes, no DB writes, no git, no sudo, no account sign-ups, no logging into any
|
||||
site. Mask secrets. Never read .env files or credentials.
|
||||
PERSIST: append a dated row to /opt/appdata/docker/research/INDEX.md if that file exists.
|
||||
FINAL message = wrap-up JSON only: status, report_path, recommendation (one paragraph), per_site[] (site, api,
|
||||
prowlarr, jackett, automation_allowed, notes), store_now[] (field, why), gotchas[3], unverified[], next_step.
|
||||
@@ -0,0 +1,59 @@
|
||||
# RA-0 — release-identity archive + capture + backfill (so saved .torrent files can be deleted) (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
Bounded background agent, media_pipeline (personal_projects #61; future_features #11 = later Prowlarr build).
|
||||
Budget: --max-turns 55. Same tool call twice with identical arguments → STOP with a partial wrap-up.
|
||||
|
||||
OWNER DECISION 2026-10-01 21:29: Prowlarr option A chosen, but ONLY do now the steps needed so the owner can safely
|
||||
delete the saved .torrent files; the Prowlarr build itself is deferred (future_features #11).
|
||||
Read first: /opt/appdata/docker/research/source_reacquisition_2026-10-01.md ("store_now" list + RA-0 phase).
|
||||
Known: download_jobs has infohash on only 1708 of 3524 rows (Movies 18 of 455).
|
||||
|
||||
GOAL: before any .torrent file is deleted, every torrent the owner has ever kept (saved .torrent files, qBittorrent
|
||||
BT_backup on BOTH qBittorrent instances — public `qbittorrent` and `qbittorrent-private` — and autobrr history) has
|
||||
its identity preserved in Postgres, and future downloads capture it automatically.
|
||||
|
||||
DECISIONS (pre-made):
|
||||
- D1 Credential-free source of truth: parse the .torrent FILES directly (BT_backup dirs + the owner's saved-.torrent
|
||||
folder(s) — locate them read-only via `docker inspect` mounts of the qbittorrent/qui/autobrr containers and the
|
||||
compose dirs; ask nothing). Write a stdlib-only Python bencode parser (no pip installs). Per torrent extract:
|
||||
infohash v1 (sha1 of the bencoded info dict) and v2 if present, name, total size, file list (path + size), comment,
|
||||
created by, creation date, tracker HOSTS only (strip passkeys/paths/query — NEVER store a full announce URL), private
|
||||
flag, source file path. Never print or store passkeys/announce URLs/cookies.
|
||||
- D2 autobrr history: read autobrr's sqlite DB READ-ONLY (copy it to your scratchpad first, query the copy) for
|
||||
release name, indexer, torrent id/guid/info url (strip any key/passkey query params), filter, timestamp; join on
|
||||
infohash or release name. If the DB is not readable without root → `⏸ OWNER-COMMAND-REQUEST`.
|
||||
- D3 Storage: a NEW table media_pipeline.release_identity (do not alter download_jobs in this run): id, infohash_v1
|
||||
(unique), infohash_v2, release_name, total_size, files jsonb, comment, created_by, creation_date, tracker_hosts
|
||||
text[], private boolean, indexer, indexer_torrent_id, info_url_sanitized, qbit_instance, source ('bt_backup'|
|
||||
'saved_torrent'|'autobrr'|'download_jobs'), first_seen, download_job_id (nullable FK-less int, matched by infohash
|
||||
or name). Write the DDL as a migration file; do NOT run it on prod. Run it on the SANDBOX DB on server-01
|
||||
(media-pipeline-sandbox-postgres-1, db media_pipeline_sandbox, user sandbox) and load the full backfill there to
|
||||
prove it.
|
||||
- D4 Backfill artifact: generate a single SQL (or COPY CSV + load script) with every row; keep a JSONL archive too
|
||||
(the same data) under /opt/appdata/docker/docker-compose/media-downloader-local/release-identity/ (create the dir).
|
||||
The main session reviews and applies the DDL + backfill to prod afterwards.
|
||||
- D5 Capture going forward: in media-downloader-local (find the code path that copies a finished torrent), add a
|
||||
call that records the torrent's identity into release_identity at copy time, reading the .torrent from qBittorrent's
|
||||
BT_backup by infohash (or the qBittorrent API only if the code already uses it with existing creds). Edit ONLY the
|
||||
sandbox twin (/opt/appdata/docker/docker-compose/server-01/media-pipeline-sandbox/media-downloader-local.py), scp it
|
||||
to the same path on server-01, add unit tests (fake .torrent built in tmp), run the full sandbox test suite. If a
|
||||
sandbox e2e is cheap (seedbox fixture → downloader-sandbox), run it and show the release_identity row. No live edit.
|
||||
- D6 Coverage report (the owner's go/no-go for deleting .torrent files): for every saved .torrent file → is its
|
||||
infohash in the backfill? For every download_jobs row → identity found or not (by infohash, else name). List any
|
||||
.torrent that could NOT be parsed. Verdict: SAFE_TO_DELETE (100% of saved .torrent files archived) or not, plus the
|
||||
exact list of gaps.
|
||||
|
||||
HARD RULES: read-only on every live container and file (cp to scratchpad for parsing is fine); NO deletion of any
|
||||
.torrent; no prod DB writes (SELECT only); no container restarts except media-pipeline-sandbox-* (log ELEVATED);
|
||||
no git; no sudo (`⏸ OWNER-COMMAND-REQUEST` block: host, command, why, paste_back, resume_at); never read .env files
|
||||
or print secrets; strip passkeys everywhere (grep your own outputs for 'passkey', '/announce?', 'torrent_pass' and
|
||||
32+ hex query params before finishing — must be 0 hits).
|
||||
PERSIST: append "## RA-0 — 2026-10-01 (background agent)" to
|
||||
/opt/appdata/docker/docker-compose/media-downloader-local/.claude/context.md; run
|
||||
python3 /opt/appdata/docker/.claude/scripts/embed_memory_dir.py --only-recent 5.
|
||||
FINAL message = wrap-up JSON only: status, project "media_pipeline", torrent_sources[] (path, host, count),
|
||||
parsed_count, unparsed[], autobrr_rows_joined, migration_path, backfill_path, archive_path, sandbox_load_proof
|
||||
(row count), capture_change_summary, tests_passed, coverage {saved_torrents_total, archived, gaps[]},
|
||||
download_jobs_coverage {total, with_identity}, verdict (SAFE_TO_DELETE|NOT_YET), passkey_scan_hits (must be 0),
|
||||
apply_commands (prod DDL + backfill, for the main session), actions_taken[], actions_failed[], unverified_claims[],
|
||||
next_step, notes.
|
||||
@@ -0,0 +1,57 @@
|
||||
# V3 — Voice Chat laptop client (push-to-talk → Whisper → Claude Code; replies → Chatterbox) (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
Bounded background agent, personal_projects #214. Budget: --max-turns 60. Same tool call twice with identical
|
||||
arguments → STOP with a partial wrap-up.
|
||||
|
||||
READ FIRST (design is decided — do not re-design): /opt/appdata/docker/agent-builder/voice_chat_build_plan.md, then
|
||||
/opt/appdata/docker/research/voice_stt_v1_2026-09-28.md and voice_tts_engine_2026-09-28.md, and the #214 notes:
|
||||
PG=$(docker ps --format '{{.Names}}' | grep '^postgres-'); docker exec "$PG" psql -U postgres -d personal_projects -At -c "SELECT notes FROM projects WHERE id=214"
|
||||
If the build plan's laptop section conflicts with the live servers below, the live servers win; report the conflict.
|
||||
|
||||
LIVE SERVERS (verified 2026-10-01): STT voice-stt on server-01 http://192.168.1.90:8300 (read its API from the
|
||||
research doc / `curl -s http://192.168.1.90:8300/docs` or openapi.json); TTS voice-tts (Chatterbox-Turbo) on
|
||||
server-01 http://192.168.1.90:8004 — streaming POST /tts (stream:true), OpenAI POST /v1/audio/speech, reference
|
||||
voice jarvis.wav (see /data/docker-compose/voice-tts/measure.py on server-01 for working request bodies). Measured:
|
||||
first audio 0.65-0.76 s streaming; first call after start ~10 s warm-up. Both are LAN-only, no auth.
|
||||
LAPTOP: ssh administrator@192.168.1.89 (key auth works, BatchMode). Hostname omarchy, Arch-based (Omarchy,
|
||||
Hyprland), Python 3.14, PipeWire (pw-record/pw-play), tmux, Claude Code installed (~/.claude exists).
|
||||
|
||||
BUILD (on the laptop, all user-level, no sudo):
|
||||
1. ~/voice-chat/ project: a Python venv (pip into the venv is fine), the push-to-talk client:
|
||||
- toggle mode via a small CLI `voice-chat ptt` (start/stop recording on each invocation, state in
|
||||
$XDG_RUNTIME_DIR) — the keybind will call it; record 16 kHz mono WAV with pw-record from the default source;
|
||||
- on stop: POST to voice-stt, get text; type it into the tmux pane running Claude Code: target configurable
|
||||
(default: the pane whose current command is `claude`; find with `tmux list-panes -a -F ...`), using
|
||||
`tmux send-keys -l` for the text, then Enter only if config `auto_submit=true` (default false → owner reviews);
|
||||
- a desktop notification (notify-send if present) for recording on/off and errors.
|
||||
2. TTS side: a Claude Code **Stop** hook script (`~/voice-chat/hooks/speak_last_reply.py`) that reads the hook JSON
|
||||
from stdin, extracts the last assistant message text (check the current Claude Code hook input schema in the
|
||||
docs — https://docs.claude.com/en/docs/claude-code/hooks — do not guess field names), strips code blocks/tables
|
||||
to a speakable summary (first ~2 sentences or ≤ 400 chars), streams it from Chatterbox and plays with pw-play.
|
||||
Non-blocking (spawn + return immediately), never fails the hook (exit 0), mute file switch
|
||||
($XDG_RUNTIME_DIR/voice-chat.mute), and skips when the reply is empty.
|
||||
3. A user systemd unit ONLY if the design needs a daemon; otherwise none. Prefer no daemon.
|
||||
4. STAGE, do NOT activate: (a) the Hyprland keybind line (find the user's Hyprland config include file used by
|
||||
Omarchy for user bindings; write the proposed line to ~/voice-chat/STAGED_hyprland_bind.conf and the exact
|
||||
include/edit instruction), (b) the Claude Code hook JSON snippet to ~/voice-chat/STAGED_claude_settings_hook.json
|
||||
with the exact place it goes in ~/.claude/settings.json. Do NOT edit ~/.claude/settings.json or Hyprland config.
|
||||
SELF-TEST (no human voice needed):
|
||||
- STT round trip: generate a WAV of a known sentence with Chatterbox (server-01), resample to 16 kHz mono, run the
|
||||
client's stop-path on that file → assert transcript ≈ sentence (word error rate ≤ 10 %).
|
||||
- tmux injection: start a throwaway tmux session running `cat` as a stand-in (NOT claude), point the client at it,
|
||||
assert the text arrives; kill the session.
|
||||
- Hook: feed a sample hook JSON (built from the documented schema) to the hook script with the mute file set
|
||||
(assert it exits 0 fast and logs what it would say) and once unmuted with output to a file instead of pw-play
|
||||
(assert a valid WAV of plausible length). Do not play audio aloud.
|
||||
- Latency: measure STT time for a 5 s clip and TTS time-to-first-chunk from the laptop.
|
||||
HARD RULES: no sudo (if a system package is truly required → `⏸ OWNER-COMMAND-REQUEST` block: host, command, why,
|
||||
paste_back, resume_at); no edits outside ~/voice-chat on the laptop; never touch the running Claude Code session or
|
||||
~/.claude/settings.json; no audio playback; no server-side changes (server-01 containers untouched); no git; mask
|
||||
secrets (there should be none).
|
||||
DELIVERABLES: ~/voice-chat/ with README.md (install/activate steps for the owner: keybind + hook, how to mute,
|
||||
how to change the tmux target, troubleshooting), tests, staged files. Copy the README to
|
||||
/opt/appdata/docker/docker-compose/voice-tts/LAPTOP_CLIENT_README.md on primary (scp from the laptop).
|
||||
FINAL message = wrap-up JSON only: status, project "voice-chat", files_built[], staged[] (path, what, where it
|
||||
goes), self_tests[] (name, result, numbers), latency {stt_5s_s, tts_first_chunk_s}, owner_steps[] (exact
|
||||
commands/edits to activate), design_conflicts[], actions_taken[], actions_failed[], unverified_claims[], next_step,
|
||||
notes.
|
||||
@@ -0,0 +1,58 @@
|
||||
# V3b — Voice Chat: Claude Code on PRIMARY + "ready gate" before speaking (spawned 2026-10-01, main session 771b2744)
|
||||
|
||||
Bounded background agent, personal_projects #214. Budget: --max-turns 60. Same tool call twice with identical
|
||||
arguments → STOP with a partial wrap-up.
|
||||
|
||||
STATE: V3 built ~/voice-chat on the LAPTOP (ssh administrator@192.168.1.89, key auth; Omarchy/Hyprland, Python 3.14,
|
||||
PipeWire, tmux) assuming Claude Code runs on the laptop. Read ~/voice-chat/README.md and code on the laptop first,
|
||||
and the build plan at /home/administrator/Desktop/claude/agent-builder/voice_chat_build_plan.md (primary-hosted
|
||||
topology). Servers: STT http://192.168.1.90:8300, TTS http://192.168.1.90:8004 (Chatterbox, jarvis.wav).
|
||||
|
||||
OWNER FACTS + DECISIONS (2026-10-01 22:52):
|
||||
- The owner runs Claude Code ON PRIMARY (192.168.1.88, host Daily-Driver-PC, user administrator), in a terminal on
|
||||
the laptop that is SSH'd into primary. Audio in/out happens on the LAPTOP.
|
||||
- D1 Input path: laptop push-to-talk (existing) → STT → text is typed into the Claude Code tmux pane ON PRIMARY via
|
||||
`ssh administrator@192.168.1.88 tmux send-keys -t <target> -l <text>` (no Enter unless auto_submit). Target
|
||||
discovery runs on primary (pane whose current command is claude). If the owner's Claude Code on primary is NOT in
|
||||
tmux today, document the one-time change (run claude inside `tmux new -As claude`) — do not change it yourself.
|
||||
Laptop→primary SSH: verify key auth from the laptop to primary works (`ssh -o BatchMode=yes administrator@192.168.1.88 true`
|
||||
from the laptop). If it doesn't, STAGE the exact key setup steps for the owner (no changes to authorized_keys by you).
|
||||
- D2 Output path: the Claude Code Stop hook runs ON PRIMARY (~/voice-chat-hook/ on primary, stdlib Python, venv not
|
||||
needed). It extracts the speakable summary (reuse V3's logic) and delivers it to the LAPTOP; the laptop fetches TTS
|
||||
and plays. Delivery = a tiny queue: primary appends a JSON line (text, session id, ts) to the laptop via
|
||||
`ssh administrator@192.168.1.89 'voice-chat enqueue'` (stdin) — non-blocking, never fails the hook (exit 0 always,
|
||||
short timeout), and falls back silently if the laptop is off. Primary→laptop SSH key auth exists (the main session
|
||||
uses it). No listening network ports.
|
||||
- D3 READY GATE (new owner requirement): the owner may be watching TV with headphones OFF. Replies must NOT play
|
||||
until the owner signals ready. Modes in config: `gate = "ready_key"` (default) | `"immediate"`.
|
||||
In ready_key mode: when a reply is queued, the laptop shows a desktop notification "Claude has a reply (N waiting)"
|
||||
plus an optional short soft chime (config `chime=false` default) and waits. The owner presses a ready key (stage a
|
||||
Hyprland bind, e.g. SUPER+CTRL+K, calling `voice-chat ready`) → the queue plays in order. Additional commands:
|
||||
`voice-chat skip` (drop queued), `voice-chat replay` (repeat last), `voice-chat gate immediate|ready_key`.
|
||||
Queued items expire after a configurable time (default 30 min) but stay listed in `voice-chat status`.
|
||||
Push-to-talk itself also counts as ready (if the owner starts talking, queued replies play first? NO — decided:
|
||||
PTT does not auto-play; only the ready key plays).
|
||||
- D4 Keep everything user-level, no sudo, no daemons unless needed (a user systemd unit for the queue player on the
|
||||
laptop is acceptable if required; prefer none: `voice-chat ready` plays synchronously).
|
||||
- STAGE, do NOT activate: the primary Claude Code hook (~/.claude/settings.json ON PRIMARY — produce the snippet +
|
||||
an installer like V3's install_hook.py that the owner runs; do NOT run it on the real file; NOTE the main session's
|
||||
own Claude Code on primary uses that settings file, so activation must be the owner's choice), and the new
|
||||
Hyprland binds on the laptop (append-ready snippet). The laptop-side V3 hook (laptop ~/.claude) is no longer the
|
||||
primary path — keep it but mark it optional in the README.
|
||||
SELF-TESTS (no audio played aloud, no human voice):
|
||||
1. Input: Chatterbox-synthesised sentence → laptop STT → injected into a throwaway tmux session ON PRIMARY running
|
||||
`cat` (create it with `tmux new -d -s voicechat-selftest cat` on primary; kill it after) → assert text arrived.
|
||||
2. Output + gate: feed a sample Stop-hook JSON (documented schema; V3 already fetched it) to the primary hook → assert
|
||||
it returns < 0.2 s with exit 0 → assert the laptop queue has 1 item and nothing played → run `voice-chat ready`
|
||||
with a file sink (VOICE_CHAT_OUT) → assert a valid WAV of plausible length → queue empty. Also test `immediate`
|
||||
mode, `skip`, expiry (with a short test expiry), and laptop-offline behaviour (point delivery at an unreachable
|
||||
host → hook still exits 0 fast).
|
||||
3. Unit tests for queue/expiry/gate on the laptop; run V3's existing tests too.
|
||||
HARD RULES: no sudo (`⏸ OWNER-COMMAND-REQUEST` block if truly needed); do not touch the running Claude Code session,
|
||||
~/.claude/settings.json on either host, Hyprland config, authorized_keys, or any server container; only write in
|
||||
~/voice-chat (laptop), ~/voice-chat-hook (primary), and the README copy; no audio playback; no git.
|
||||
DELIVERABLES: updated laptop README + primary ~/voice-chat-hook/README.md; copy both to
|
||||
/opt/appdata/docker/docker-compose/voice-tts/ (LAPTOP_CLIENT_README.md, PRIMARY_HOOK_README.md).
|
||||
FINAL message = wrap-up JSON only: status, project "voice-chat", files_built[], staged[] (path, what, where, host),
|
||||
self_tests[] (name, result, numbers), owner_steps[] (exact, per host, in order), ssh_auth {laptop_to_primary,
|
||||
primary_to_laptop}, design_conflicts[], actions_taken[], actions_failed[], unverified_claims[], next_step, notes.
|
||||
Reference in New Issue
Block a user