harness: scratch graduation pipeline (push side + spec) + flarum first test case

- graduation-push.sh: tar+scp scratch -> BEAST graduation-inbox over Tailscale (decoupled
  from /save, soft-fail if BEAST off). Tested: 241 files -> BEAST.
- docs/graduation-pipeline.md: full spec (push -> Ollama triage on BEAST GPU via API ->
  reviewed sanitize+git-mv). Secrets never enter git; ride the encrypted link to BEAST only.
- tmp-promotion-check.sh: rewritten pure-builtin (0.4s) after the per-file grep/fork loop
  hung /save for 4 min on Windows at ~240 scratch files. Deep triage moves to the pipeline.
- forum-post: GRADUATED the canonical flarum poster from scratch ->
  skills/forum-post/scripts/flarum-post.py (s9e markdown->XML + DB insert machinery), with
  the hardcoded IX SSH + Flarum DB passwords swapped to vault lookups. First pipeline test case.
- Vaulted the Flarum DB cred (services/flarum-community.sops.yaml) + sanitized the two
  plaintext copies in forum-post.md.
- errorlog: logged the WSL-stub correction + BEAST-Ollama-CPU(vram=0) finding + the
  promotion-check hang, all via the new log helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 12:55:48 -07:00
parent d9c710be31
commit 9581d87589
6 changed files with 383 additions and 33 deletions

View File

@@ -25,7 +25,7 @@ determine what to post (the most recent technical problem solved, fix documented
| DB host | localhost (on IX) | | DB host | localhost (on IX) |
| DB name | azcompu_flarum | | DB name | azcompu_flarum |
| DB user | azcompu_flarum | | DB user | azcompu_flarum |
| DB pass | `Fl@rum2026!CGS` | | DB pass | vault: `services/flarum-community.sops.yaml credentials.db_password` |
| IX SSH | root@172.16.3.10 — password from vault: `infrastructure/ix-server.sops.yaml credentials.password` | | IX SSH | root@172.16.3.10 — password from vault: `infrastructure/ix-server.sops.yaml credentials.password` |
| Admin user_id | 1 (MikeSwanson) | | Admin user_id | 1 (MikeSwanson) |
@@ -219,7 +219,7 @@ The closing nowdoc marker `FLARUM_POST_XML_END;` must be at column 0 with no lea
<?php <?php
ini_set('display_errors', 1); error_reporting(E_ALL); ini_set('display_errors', 1); error_reporting(E_ALL);
$dsn = 'mysql:host=localhost;dbname=azcompu_flarum;charset=utf8mb4'; $dsn = 'mysql:host=localhost;dbname=azcompu_flarum;charset=utf8mb4';
$pdo = new PDO($dsn, 'azcompu_flarum', 'Fl@rum2026!CGS', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); $pdo = new PDO($dsn, 'azcompu_flarum', '<DB_PASS from vault services/flarum-community.sops.yaml>', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$user_id = 1; $tag_id = %%TAG_ID%%; $user_id = 1; $tag_id = %%TAG_ID%%;
$title = %%TITLE_PHP%%; $title = %%TITLE_PHP%%;

View File

@@ -0,0 +1,84 @@
# Scratch Graduation Pipeline (spec)
Status: **draft / in progress** (2026-06-15). Push side built + tested; triage validated on
the flarum test case; scheduled-on-BEAST wiring + execute helper are the remaining work.
## Problem
Scratch dirs (`tmp/`, `temp/`, `.claude/tmp/`) are gitignored, so anything in them is invisible
to git and lost on cleanup. The old approach — a **synchronous** `tmp-promotion-check.sh` run
inside `/save` and `/scc` — had two fatal flaws:
1. **Too slow on Windows.** At ~240 scratch files it forked `basename`/`wc`/`grep -r` per file;
the "referenced in a session log" check recursed `clients/` + `projects/` (Rust `target/`,
`node_modules/`, `.git`) **once per file** and hung `/save` for **4 minutes** (errorlog 2026-06-15).
2. **Too dumb.** Extension/size heuristics can't answer the real question — *which* of
`flarum_do_insert.py` / `do_insert2.py` / `search_insert.py` is canonical, what's a superseded
debug dupe, what holds secrets, where each belongs. That's semantic judgment.
The interim `tmp-promotion-check.sh` is now a fast (0.4s) pure-builtin "N scripts in scratch" nudge.
The real triage is **offloaded and asynchronous**, per this spec.
## Architecture
```
workstation BEAST (GURU-BEAST-ROG, best GPU) any Claude session
----------- -------------------------------- ------------------
graduation-push.sh Ollama @ :11434 (GPU) review proposal
tar scratch ──SCP/Tailscale──▶ graduation-inbox/<machine>/*.tgz sanitize secrets
(soft-fail if BEAST off) graduation-triage (Ollama classify) git mv keepers
─▶ proposal manifest ──coord msg/todo──▶ delete junk → commit
```
1. **Push** (`graduation-push.sh`, built): tars scratch and `scp`s ONE tarball to
`guru@100.101.122.4:graduation-inbox/<machine>/scratch-<utc>.tgz` over Tailscale. Decoupled
from `/save`; soft-fails if BEAST is unreachable. Centralizes every machine's scratch on the
GPU box (archive + lets BEAST batch-process even when the origin machine is off).
2. **Triage** (Ollama on BEAST's GPU): for each file, classify
`{disposition: graduate|delete|keep-data, canonical?, superseded_by, has_secrets, suggested_home, why}`.
Emits a **proposal manifest** (the supersession/secret reasoning the old heuristics couldn't do).
The orchestration can run **on BEAST** (Git-bash, scheduled) or on **any machine** against
BEAST's Ollama API — the GPU is reached over the HTTP API either way.
3. **Review + execute**: a Claude session (or human) reads the manifest, **sanitizes secrets**
(hardcoded creds → vault lookups), `git mv`s keepers to permanent homes, deletes junk, commits.
*Ollama proposes, human/Claude disposes* (same contract as memory-dream + the Tier-0 routing rule).
## Transport / environment facts (verified 2026-06-15)
- BEAST = `guru-beast-rog`, Tailscale `100.101.122.4`. SSH key auth works as **`guru`** (no password).
- BEAST default SSH shell = **cmd.exe**; home `C:\Users\guru`. The harness/triage run under
**Git-for-Windows MSYS bash** — NOT WSL. (`bash` on PATH resolves to the WindowsApps WSL stub;
invoke Git-bash explicitly. The WSL stub also can't reach the Windows-host Ollama on localhost —
another reason to avoid it.)
- **Ollama** runs on BEAST's Windows side, bound so it's reachable fleet-wide over Tailscale at
`http://100.101.122.4:11434`. Models incl. `qwen3:32b`, `qwen3.6:latest` (36B), `gemma3:27b`,
`codestral:22b`, `qwen3:14b`, `nomic-embed-text`.
- Inbox: `C:\Users\guru\graduation-inbox\<machine>\` (cmd path) — per-machine namespaced.
## Security (non-negotiable)
- **Secrets never enter git.** Raw scratch can contain hardcoded creds (the flarum scripts hold the
IX root SSH password). It rides the WireGuard-encrypted Tailscale/SSH link and lands ONLY on BEAST
(trusted). The transport is deliberately NOT the git repo or a multi-tenant store.
- **Sanitize before commit.** Any file graduated into a tracked home gets hardcoded secrets swapped
for vault lookups first (`vault.sh get-field ...`). harness-guard would block a plaintext-secret commit.
- **Manifest-only returns.** Only the proposal manifest comes back toward git — never the raw files.
## Components
| Piece | Path | State |
|---|---|---|
| Push | `.claude/scripts/graduation-push.sh` | built + tested (241 files → BEAST) |
| Interim nudge | `.claude/scripts/tmp-promotion-check.sh` | fast builtin-only (0.4s) |
| Triage | `.claude/scripts/graduation-triage.*` | validated ad-hoc on flarum; productize next |
| Execute | manual (Claude session) | flarum = first test case |
| Schedule | BEAST cron/loop calling triage | TODO |
| Return | coord message/todo to origin machine | TODO |
## Open items
- Productize `graduation-triage` (general file loop + Ollama classify + manifest) and a `--execute`
helper that sanitizes + `git mv`s per an approved manifest.
- Wire a scheduled triage run on BEAST (or a `/loop`) + coord-message return.
- Decide retention/cleanup of the BEAST inbox + auto-deleting obvious junk to keep scratch bounded.
- Consider dropping `tmp-promotion-check` from `/save` entirely once the pipeline is routine.

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# graduation-push.sh — ship this machine's scratch (tmp/, temp/, .claude/tmp/) to BEAST's
# graduation inbox over Tailscale+SSH, for async Ollama triage. Part of the scratch
# graduation pipeline (see .claude/TEMP_GRADUATION.md / graduation-pipeline spec).
#
# DECOUPLED from /save and SOFT-FAIL: if BEAST is off/unreachable it warns and exits 0 —
# it must never block a save or a commit.
#
# Transport: tar the scratch -> scp ONE tarball to guru@<beast>:graduation-inbox/<machine>/.
# Secrets in scratch (e.g. hardcoded creds) ride the WireGuard-encrypted Tailscale/SSH link
# and land ONLY on BEAST (a trusted fleet box) — they never enter git. The graduation step
# sanitizes secrets (-> vault lookups) before anything is committed to a permanent home.
#
# BEAST: guru@100.101.122.4 (key auth as `guru`), default SSH shell = cmd.exe, home C:\Users\guru.
set -u
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
SSH="/c/Windows/System32/OpenSSH/ssh.exe"; [ -x "$SSH" ] || SSH="ssh"
SCP="/c/Windows/System32/OpenSSH/scp.exe"; [ -x "$SCP" ] || SCP="scp"
BEAST="guru@100.101.122.4"
CTO=8
MACHINE="$(jq -r '.machine_name // .hostname // empty' "$ROOT/.claude/identity.json" 2>/dev/null)"
[ -z "$MACHINE" ] && MACHINE="$(hostname)"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
# Collect existing scratch dirs + count files.
DIRS=(); for d in tmp temp .claude/tmp; do [ -d "$ROOT/$d" ] && DIRS+=("$d"); done
[ "${#DIRS[@]}" -eq 0 ] && { echo "[INFO] graduation-push: no scratch dirs — nothing to send"; exit 0; }
N=$(find "${DIRS[@]/#/$ROOT/}" -type f ! -name '.gitkeep' ! -name '.grad-*.tgz' 2>/dev/null | wc -l | tr -d ' ')
[ "${N:-0}" -eq 0 ] && { echo "[INFO] graduation-push: scratch empty — nothing to send"; exit 0; }
# Reachability gate (fast, BatchMode so it can't prompt).
if ! "$SSH" -o BatchMode=yes -o ConnectTimeout=$CTO "$BEAST" "exit 0" >/dev/null 2>&1; then
echo "[WARN] graduation-push: BEAST ($BEAST) unreachable — skipped; scratch stays local" >&2
exit 0
fi
# Tar to an OS-temp path OUTSIDE the scratch dirs (avoid taring our own tarball).
TARBALL="$(mktemp -t gradpush-XXXXXX 2>/dev/null).tgz"; [ -z "$TARBALL" ] && TARBALL="/tmp/gradpush-$STAMP.tgz"
if ! tar -czf "$TARBALL" -C "$ROOT" "${DIRS[@]}" 2>/dev/null; then
echo "[WARN] graduation-push: tar failed" >&2; rm -f "$TARBALL" 2>/dev/null; exit 0
fi
# Ensure remote per-machine inbox (cmd.exe: mkdir makes intermediate dirs; 2>nul ignores 'exists').
"$SSH" -o ConnectTimeout=$CTO "$BEAST" "mkdir graduation-inbox\\$MACHINE 2>nul & exit 0" >/dev/null 2>&1
REMOTE="graduation-inbox/$MACHINE/scratch-$STAMP.tgz"
if "$SCP" -o ConnectTimeout=$CTO -q "$TARBALL" "$BEAST:$REMOTE" 2>/dev/null; then
echo "[OK] graduation-push: sent $N scratch file(s) -> BEAST:$REMOTE"
else
echo "[WARN] graduation-push: scp to BEAST failed" >&2
bash "$ROOT/.claude/scripts/log-skill-error.sh" "graduation-push" "scp of scratch tarball to BEAST failed (machine=$MACHINE)" >/dev/null 2>&1
fi
rm -f "$TARBALL" 2>/dev/null
exit 0

View File

@@ -27,40 +27,17 @@ mapfile -t FILES < <(
echo "[INFO] Promotion check: ${#FILES[@]} file(s) in scratch dirs (gitignored — NOT committed)." echo "[INFO] Promotion check: ${#FILES[@]} file(s) in scratch dirs (gitignored — NOT committed)."
echo " Graduate anything worth keeping before it's lost. Guide: .claude/TEMP_GRADUATION.md" echo " Graduate anything worth keeping before it's lost. Guide: .claude/TEMP_GRADUATION.md"
# PURE-BUILTIN loop (no per-file subprocesses) — `basename`/`wc` forks ×N hung this for
# 20s+ on Windows Git-bash at ~240 scratch files (fork is expensive). Flag scripts by
# extension only; deep triage (doc size, "is it referenced", what's it for) is deferred to
# the async Ollama graduation pass (see TEMP_GRADUATION.md). Keep this O(N) and fork-free.
candidates=0 candidates=0
for f in "${FILES[@]}"; do for f in "${FILES[@]}"; do
base="$(basename "$f")" case "${f##*/}" in
reason="" *.py|*.sh|*.ps1|*.psm1|*.js|*.rb|*.pl|*.php)
echo " [GRADUATE?] $f (script)"
# Script-like files: reusable automation worth a permanent home. candidates=$((candidates + 1)) ;;
case "$base" in
*.py|*.sh|*.ps1|*.psm1|*.js|*.rb|*.pl) reason="script" ;;
esac esac
# Substantial docs (audit reports, dossiers) — size threshold ~4 KB.
if [ -z "$reason" ]; then
case "$base" in
*.md|*.csv)
sz=$(wc -c < "$f" 2>/dev/null || echo 0)
[ "${sz:-0}" -ge 4096 ] && reason="doc ($((sz/1024))KB)"
;;
esac
fi
# Referenced in a session log / doc → clearly load-bearing.
# Scope to markdown only and skip heavy build/dep/vcs trees — a bare `grep -r`
# over projects/ (Rust target/, node_modules/, .git) hangs for minutes per file.
if grep -rqlF "$f" --include='*.md' \
--exclude-dir=.git --exclude-dir=node_modules --exclude-dir=target \
--exclude-dir=dist --exclude-dir=build --exclude-dir=.next --exclude-dir=vendor \
session-logs/ clients/ projects/ 2>/dev/null; then
reason="${reason:+$reason, }referenced"
fi
if [ -n "$reason" ]; then
echo " [GRADUATE?] $f ($reason)"
candidates=$((candidates + 1))
fi
done done
if [ "$candidates" -eq 0 ]; then if [ "$candidates" -eq 0 ]; then

View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python3
"""flarum-post.py — post a markdown article to community.azcomputerguru.com (Flarum) by
converting it to Flarum's s9e TextFormatter XML and inserting into the Flarum MySQL DB over
SSH to the IX server. The canonical machinery behind the /forum-post skill.
GRADUATED 2026-06-15 from .claude/tmp scratch (the first test case of the scratch-graduation
pipeline — see .claude/docs/graduation-pipeline.md). Secrets that were HARDCODED in the
scratch original (IX root SSH password, Flarum DB password) now load from the SOPS vault at
runtime, so this file is safe to commit.
NEXT STEP (Mike's plans): genericize — take TITLE / CONTENT_MD / SLUG / tag_id as inputs
(skill Phase 1) instead of the hardcoded demo article below. The converter + insert flow are
already generic; only the CONTENT_MD/TITLE/SLUG block is example data.
"""
import os, re, subprocess
import paramiko
# ---- secrets from vault (never hardcode) ----------------------------------------------------
def _root():
return os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
def vault_get(path, field):
r = _root()
p = subprocess.run(["bash", os.path.join(r, ".claude", "scripts", "vault.sh"),
"get-field", path, field],
capture_output=True, text=True, timeout=30)
v = (p.stdout or "").strip()
if not v:
raise SystemExit(f"[ERROR] vault_get {path} {field} failed: {(p.stderr or '').strip()[:200]}")
return v
HOST = "172.16.3.10"
SSH_USER = "root"
SSH_PASS = vault_get("infrastructure/ix-server.sops.yaml", "credentials.password")
DB_PASS = vault_get("services/flarum-community.sops.yaml", "credentials.db_password")
# ---- DEMO article (replace with skill Phase 1 inputs when genericizing) ---------------------
CONTENT_MD = """\
We did a server-wide audit today and found 7 production WordPress sites with search indexing silently disabled. The sites looked completely normal to visitors. Google couldn't see any of them.
Here's the setting, why it gets left on, and how to audit a whole cPanel server at once.
## The Setting
In WordPress: **Settings -> Reading -> "Discourage search engines from indexing this site"**
When checked, WordPress adds `<meta name="robots" content="noindex,follow">` to every page and sets `blog_public` to `0` in `wp_options`. When unchecked, `blog_public = 1`. One row in one table; no other indicator anywhere on the site.
## The Fix
One SQL update per site:
```sql
UPDATE wp_options SET option_value = '1' WHERE option_name = 'blog_public';
```
After updating, verify by fetching the page source and confirming there's no `<meta name="robots" content="noindex">` in the `<head>`."""
TITLE = 'WordPress "Discourage Search Engines" Setting -- How 7 Production Sites Lost Their Indexing'
SLUG = "wordpress-discourage-search-engines-setting-how-7-production-sites-lost-their-indexing"
TAG_ID = 7 # How-Tos & Tips
# ---- markdown -> Flarum s9e TextFormatter XML -----------------------------------------------
def xml_escape(t):
return t.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
def inline_to_xml(text):
result = ""
i = 0
while i < len(text):
if text[i:i + 2] == "**":
end = text.find("**", i + 2)
if end != -1:
result += "<STRONG><s>**</s>" + inline_to_xml(text[i + 2:end]) + "<e>**</e></STRONG>"
i = end + 2
continue
if text[i] == "`":
end = text.find("`", i + 1)
if end != -1:
result += "<C><s>`</s>" + xml_escape(text[i + 1:end]) + "<e>`</e></C>"
i = end + 1
continue
if text[i] == "*" and text[i:i + 2] != "**":
j = i + 1
end = -1
while j < len(text):
if text[j] == "*" and text[j:j + 2] != "**":
end = j
break
j += 1
if end != -1:
result += "<EM><s>*</s>" + xml_escape(text[i + 1:end]) + "<e>*</e></EM>"
i = end + 1
continue
result += xml_escape(text[i])
i += 1
return result
def md_to_s9e(md):
lines = md.split("\n")
elements = []
i = 0
while i < len(lines):
line = lines[i]
if not line.strip():
i += 1
continue
if line.startswith("## "):
elements.append("<H2><s>## </s>" + inline_to_xml(line[3:]) + "</H2>")
i += 1
elif line.startswith("### "):
elements.append("<H3><s>### </s>" + inline_to_xml(line[4:]) + "</H3>")
i += 1
elif line.startswith("- "):
items = []
while i < len(lines) and lines[i].startswith("- "):
items.append("<LI><s>- </s>" + inline_to_xml(lines[i][2:]) + "</LI>")
i += 1
elements.append("<LIST>" + "\n".join(items) + "</LIST>")
elif re.match(r"^\d+\. ", line):
items = []
while i < len(lines) and re.match(r"^\d+\. ", lines[i]):
m = re.match(r"^(\d+)\. (.*)", lines[i])
items.append("<LI><s>" + m.group(1) + ". </s>" + inline_to_xml(m.group(2)) + "</LI>")
i += 1
elements.append('<LIST type="decimal">' + "\n".join(items) + "</LIST>")
elif line.startswith("```"):
lang = line[3:].strip()
code_lines = []
i += 1
while i < len(lines) and not lines[i].startswith("```"):
code_lines.append(xml_escape(lines[i]))
i += 1
i += 1
code_body = "\n".join(code_lines)
if lang:
elements.append(f'<CODE lang="{lang}"><s>```{lang}</s><i>\n</i>' + code_body + "\n<e>```</e></CODE>")
else:
elements.append("<CODE><s>```</s><i>\n</i>" + code_body + "\n<e>```</e></CODE>")
else:
para_lines = []
while i < len(lines) and lines[i].strip():
l = lines[i]
if (l.startswith("## ") or l.startswith("### ") or l.startswith("- ")
or l.startswith("```") or re.match(r"^\d+\. ", l)):
break
para_lines.append(l)
i += 1
elements.append("<p>" + inline_to_xml("\n".join(para_lines)) + "</p>")
return "<r>" + "\n\n".join(elements) + "</r>"
xml_content = md_to_s9e(CONTENT_MD)
print(f"[INFO] XML length={len(xml_content)}")
php_template = """<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
$dsn = 'mysql:host=localhost;dbname=azcompu_flarum;charset=utf8mb4';
$pdo = new PDO($dsn, 'azcompu_flarum', '%%DB_PASS%%', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$user_id = 1;
$tag_id = %%TAG_ID%%;
$title = %%TITLE_JSON%%;
$slug = '%%SLUG%%';
$now = date('Y-m-d H:i:s');
$content = <<<'FLARUM_POST_XML_END'
%%XML_CONTENT%%
FLARUM_POST_XML_END;
$stmt = $pdo->prepare("INSERT INTO discussions (title, comment_count, post_number_index, created_at, user_id, slug, is_private, is_approved) VALUES (?, 1, 1, ?, ?, ?, 0, 1)");
$stmt->execute([$title, $now, $user_id, $slug]);
$disc_id = $pdo->lastInsertId();
echo "Discussion ID: $disc_id\\n";
$stmt = $pdo->prepare("INSERT INTO posts (discussion_id, number, created_at, user_id, type, content, is_private, is_approved) VALUES (?, 1, ?, ?, 'comment', ?, 0, 1)");
$stmt->execute([$disc_id, $now, $user_id, $content]);
$post_id = $pdo->lastInsertId();
echo "Post ID: $post_id\\n";
$pdo->prepare("UPDATE discussions SET first_post_id=?, last_post_id=?, last_posted_at=?, last_posted_user_id=?, last_post_number=1 WHERE id=?")->execute([$post_id, $post_id, $now, $user_id, $disc_id]);
$pdo->prepare("INSERT INTO discussion_tag (discussion_id, tag_id) VALUES (?, ?)")->execute([$disc_id, $tag_id]);
echo "Done! URL: https://community.azcomputerguru.com/d/$disc_id-$slug\\n";
"""
import json as _json
php_script = (php_template
.replace("%%XML_CONTENT%%", xml_content)
.replace("%%DB_PASS%%", DB_PASS)
.replace("%%TAG_ID%%", str(TAG_ID))
.replace("%%SLUG%%", SLUG)
.replace("%%TITLE_JSON%%", _json.dumps(TITLE)))
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(HOST, username=SSH_USER, password=SSH_PASS, timeout=10)
print("[OK] SSH connected")
sftp = client.open_sftp()
with sftp.open("/tmp/flarum_post.php", "wb") as f:
f.write(php_script.encode("utf-8"))
sftp.close()
def run_chan(cmd):
chan = client.get_transport().open_session()
chan.exec_command(cmd)
chan.shutdown_write()
out = b""
while not chan.exit_status_ready():
if chan.recv_ready():
out += chan.recv(4096)
while chan.recv_ready():
out += chan.recv(4096)
return out.decode("utf-8", errors="replace"), chan.recv_exit_status()
out, rc = run_chan("php -l /tmp/flarum_post.php 2>&1")
print(f"Syntax: {out.strip()}")
out, rc = run_chan("php /tmp/flarum_post.php 2>&1")
print(f"rc={rc}\n{out}")
run_chan("rm -f /tmp/flarum_post.php")
client.close()

View File

@@ -17,6 +17,10 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure ·
<!-- Append entries below this line --> <!-- Append entries below this line -->
2026-06-15 | GURU-5070 | graduation-pipeline (BEAST Ollama) | [friction] BEAST Ollama ran inference on CPU (api/ps showed qwen3:32b AND qwen3:14b with vram=0); 32b timed out at 240s, 14b at 175s. GPU not engaged - the 'use BEAST GPU' premise needs a BEAST-side Ollama GPU config/driver fix before large-model triage is practical
2026-06-15 | GURU-5070 | graduation-pipeline (BEAST env) | [friction] assumed BEAST uses WSL because 'bash' there resolved to the WindowsApps WSL stub (uname said WSL2). BEAST runs the harness under Git-for-Windows MSYS bash like other Windows boxes; reach its Ollama via localhost (Git-bash) or the Tailscale IP. REPEAT of the documented WSL-stub-vs-Git-bash gotcha [ctx: ref=feedback_windows_bash_mapping]
2026-06-15 | GURU-5070 | tmp-promotion-check (/save,/scc) | [friction] hung for minutes: line 51 ran 'grep -rqlF <f> projects/' per scratch file, recursing Rust target/, node_modules/, .git in the guru-rmm/guru-connect submodules. Fixed: --include='*.md' + --exclude-dir for heavy trees. Stalled the /save sync behind it 2026-06-15 | GURU-5070 | tmp-promotion-check (/save,/scc) | [friction] hung for minutes: line 51 ran 'grep -rqlF <f> projects/' per scratch file, recursing Rust target/, node_modules/, .git in the guru-rmm/guru-connect submodules. Fixed: --include='*.md' + --exclude-dir for heavy trees. Stalled the /save sync behind it
2026-06-15 | GURU-5070 | memory-dream (--apply-safe) | flagged feedback_broken_backlinks_are_writeme_markers.md as an orphan and appended a DUPLICATE index line though it already had one — orphan detector likely keys on the frontmatter name: slug, not the (file.md) link target. Fix the index-line matching to compare by filename [ctx: mode=apply-safe] 2026-06-15 | GURU-5070 | memory-dream (--apply-safe) | flagged feedback_broken_backlinks_are_writeme_markers.md as an orphan and appended a DUPLICATE index line though it already had one — orphan detector likely keys on the frontmatter name: slug, not the (file.md) link target. Fix the index-line matching to compare by filename [ctx: mode=apply-safe]