Add sandbox template image with Claude configuration and plugins
build / build (push) Canceled after 0s

Carries CLAUDE.md, AGENTS.md, hooks and skills verbatim from the host, plus a
manifest of the 10 marketplaces and 17 plugins to reinstall at build time. The
plugin directories themselves are not committed: ~/.claude/plugins is 831 MB and
sits alongside credentials and transcripts, so the image is reproduced from the
manifest instead and the build needs no access to the host.

The Gitea registry is behind Cloudflare, which rejects request bodies over 100 MB
against a base image with a 325 MB layer, so the workflow pushes chunked through
regctl rather than docker push.

sbx v0.37.0 and v0.37.1 cannot consume the result: layers stacked on the base are
silently dropped (docker/sbx-releases#366). The image builds and pushes correctly
and is a no-op at runtime until that is fixed, so README points at
'ai:sbx setup' as the mechanism that works today.
This commit is contained in:
2026-07-31 08:18:30 -05:00
commit c89e4f0568
22 changed files with 2726 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Extract plain text from a document for backlog analysis.
Supports: .md .markdown .rst .txt (read), .odt / .docx (zip + XML strip),
.pdf (pdftotext if available, else pypdf/pdfplumber). Writes text to stdout.
Usage: extract.py <path>
"""
import sys, os, re, html, zipfile, shutil, subprocess
def _strip_xml(xml: str, para_close=("</text:p>", "</text:h>", "</w:p>")) -> str:
for tag in para_close:
xml = xml.replace(tag, "\n")
xml = re.sub(r"<[^>]+>", "", xml)
xml = html.unescape(xml)
lines = [ln.strip() for ln in xml.split("\n")]
return "\n".join(ln for ln in lines if ln)
def from_zip_xml(path: str, inner: str) -> str:
with zipfile.ZipFile(path) as z:
return _strip_xml(z.read(inner).decode("utf-8", "ignore"))
def from_pdf(path: str) -> str:
if shutil.which("pdftotext"):
out = subprocess.run(["pdftotext", "-layout", path, "-"],
capture_output=True, text=True)
if out.returncode == 0 and out.stdout.strip():
return out.stdout
try:
import pypdf
return "\n".join(p.extract_text() or "" for p in pypdf.PdfReader(path).pages)
except Exception:
pass
try:
import pdfplumber
with pdfplumber.open(path) as pdf:
return "\n".join(pg.extract_text() or "" for pg in pdf.pages)
except Exception:
sys.exit("PDF extraction needs `pdftotext` (poppler) or `pip install pypdf`.")
def extract(path: str) -> str:
ext = os.path.splitext(path)[1].lower()
if ext in (".md", ".markdown", ".rst", ".txt", ".text", ""):
with open(path, encoding="utf-8", errors="replace") as f:
return f.read()
if ext == ".odt":
return from_zip_xml(path, "content.xml")
if ext == ".docx":
return from_zip_xml(path, "word/document.xml")
if ext == ".pdf":
return from_pdf(path)
# last resort: try as text
try:
with open(path, encoding="utf-8") as f:
return f.read()
except UnicodeDecodeError:
sys.exit(f"Unsupported binary format: {ext}")
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit("usage: extract.py <path>")
if not os.path.isfile(sys.argv[1]):
sys.exit(f"no such file: {sys.argv[1]}")
sys.stdout.write(extract(sys.argv[1]))
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
"""Vikunja backlog helper for the sliders/feature-category model.
Idempotently ensures the 8 feature-category projects + framework labels exist
under the root project, and creates tasks from an approved JSON breakdown
(deduping, and working around the mark-done-zeros-priority quirk).
Auth/config (environment):
VIKUNJA_API_KEY required — long-lived API token
VIKUNJA_BASE_URL default https://tasks.mroberts.dev/api/v1
VIKUNJA_ROOT_PROJECT default CareEvolution
Subcommands:
ensure create missing category projects + acute/diminishing labels
projects list category projects (name -> id)
labels list labels (name -> id)
create --file T.json create tasks from an approved breakdown (see --help)
Tasks JSON: a list of objects:
{category, title, description, priority, labels:[...], done?, percent?}
`category` must be one of the 8 exact names. Missing labels are created.
Duplicates (same title + category + deployment labels) are skipped.
"""
import sys, os, json, argparse, urllib.request, urllib.error
BASE = os.environ.get("VIKUNJA_BASE_URL", "https://tasks.mroberts.dev/api/v1")
ROOT_NAME = os.environ.get("VIKUNJA_ROOT_PROJECT", "CareEvolution")
CATEGORIES = [
"End-user experience",
"Admin & operator experience",
"Developer experience & adoption",
"Data, integrations & ecosystem",
"Performance & scale",
"Reliability & observability",
"Security, privacy & compliance",
"Cost efficiency",
]
FRAMEWORK_LABELS = ["acute", "diminishing"]
def _key():
k = os.environ.get("VIKUNJA_API_KEY")
if not k:
sys.exit("VIKUNJA_API_KEY not set")
return k
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(
BASE + path, data=data, method=method,
headers={"Authorization": "Bearer " + _key(),
"Content-Type": "application/json"})
try:
resp = urllib.request.urlopen(req)
return resp.status, json.loads(resp.read() or "null")
except urllib.error.HTTPError as e:
return e.code, e.read().decode()[:300]
def get(path):
return call("GET", path)[1]
def root_id():
for p in get("/projects") or []:
if p["title"] == ROOT_NAME and p.get("parent_project_id") in (0, None):
return p["id"]
sys.exit(f"root project {ROOT_NAME!r} not found")
def all_labels():
return {l["title"]: l["id"] for l in (get("/labels") or [])}
def category_projects():
rid = root_id()
return {p["title"]: p["id"] for p in (get("/projects") or [])
if p.get("parent_project_id") == rid and p["title"] in CATEGORIES}
def ensure_label(name, cache):
if name in cache:
return cache[name]
s, l = call("PUT", "/labels", {"title": name})
if not isinstance(l, dict):
sys.exit(f"failed creating label {name!r}: {l}")
cache[name] = l["id"]
return l["id"]
def cmd_ensure(_):
rid = root_id()
existing = category_projects()
cats = dict(existing)
for c in CATEGORIES:
if c not in cats:
s, p = call("PUT", "/projects", {"title": c, "parent_project_id": rid})
if not isinstance(p, dict):
sys.exit(f"failed creating project {c!r}: {p}")
cats[c] = p["id"]
print(f"created project {c!r} -> {p['id']}", file=sys.stderr)
labels = all_labels()
for n in FRAMEWORK_LABELS:
ensure_label(n, labels)
print(json.dumps({"root": rid, "categories": cats, "labels": labels}, indent=2))
def cmd_projects(_):
print(json.dumps(category_projects(), indent=2))
def cmd_labels(_):
print(json.dumps(all_labels(), indent=2))
def _deployment_labels(labels):
return {l for l in labels if l not in FRAMEWORK_LABELS}
def cmd_create(args):
tasks = json.load(open(args.file))
if not isinstance(tasks, list):
sys.exit("tasks JSON must be a list")
rid = root_id()
cats = category_projects()
# ensure all referenced categories exist
for t in tasks:
c = t["category"]
if c not in CATEGORIES:
sys.exit(f"unknown category {c!r} (must be one of the 8)")
if c not in cats:
s, p = call("PUT", "/projects", {"title": c, "parent_project_id": rid})
cats[c] = p["id"]
label_cache = all_labels()
# cache existing tasks per category for dedup
existing = {}
for c, pid in cats.items():
existing[c] = get(f"/projects/{pid}/tasks") or []
created, skipped = [], []
for t in tasks:
c = t["category"]
pid = cats[c]
title = t["title"]
want_dep = _deployment_labels(t.get("labels", []))
dup = False
for ex in existing[c]:
if ex["title"].strip().lower() != title.strip().lower():
continue
ex_labels = {l["title"] for l in (ex.get("labels") or [])}
if want_dep.issubset(ex_labels):
dup = True
break
if dup:
skipped.append((title, c, sorted(want_dep)))
continue
prio = int(t.get("priority", 0))
s, task = call("PUT", f"/projects/{pid}/tasks", {
"title": title,
"description": (t.get("description") or "").replace("\n", "<br>"),
"priority": prio,
})
if not isinstance(task, dict):
sys.exit(f"create failed for {title!r}: {task}")
tid = task["id"]
for ln in t.get("labels", []):
call("PUT", f"/tasks/{tid}/labels", {"label_id": ensure_label(ln, label_cache)})
done = bool(t.get("done"))
pct = int(t.get("percent", 0))
if done or pct:
# re-send priority: the update replaces the task and would zero it
call("POST", f"/tasks/{tid}", {
"done": done, "percent_done": pct / 100, "priority": prio})
existing[c].append({"title": title,
"labels": [{"title": x} for x in t.get("labels", [])]})
created.append((tid, title, c, prio, sorted(want_dep), "done" if done else (f"{pct}%" if pct else "")))
print(f"created {len(created)} tasks, skipped {len(skipped)} duplicate(s)", file=sys.stderr)
for r in created:
print(f" + #{r[0]} [{r[3]}] {r[2]}: {r[1]} {r[4]} {r[5]}", file=sys.stderr)
for r in skipped:
print(f" = skip {r[1]}: {r[0]} {r[2]}", file=sys.stderr)
print(json.dumps({"created": [r[0] for r in created],
"skipped": len(skipped)}))
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
sub = ap.add_subparsers(dest="cmd", required=True)
sub.add_parser("ensure", help="create missing category projects + labels")
sub.add_parser("projects", help="list category projects")
sub.add_parser("labels", help="list labels")
c = sub.add_parser("create", help="create tasks from approved JSON")
c.add_argument("--file", required=True, help="path to tasks JSON (list)")
args = ap.parse_args()
{"ensure": cmd_ensure, "projects": cmd_projects,
"labels": cmd_labels, "create": cmd_create}[args.cmd](args)
if __name__ == "__main__":
main()