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:
@@ -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%%;
|
||||||
|
|||||||
84
.claude/docs/graduation-pipeline.md
Normal file
84
.claude/docs/graduation-pipeline.md
Normal 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.
|
||||||
55
.claude/scripts/graduation-push.sh
Normal file
55
.claude/scripts/graduation-push.sh
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
230
.claude/skills/forum-post/scripts/flarum-post.py
Normal file
230
.claude/skills/forum-post/scripts/flarum-post.py
Normal 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("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user