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
+56
View File
@@ -0,0 +1,56 @@
---
name: context7-mcp
description: This skill should be used when the user asks about libraries, frameworks, API references, or needs code examples. Activates for setup questions, code generation involving libraries, or mentions of specific frameworks like React, Vue, Next.js, Prisma, Supabase, etc.
---
When the user asks about libraries, frameworks, or needs code examples, use Context7 to fetch current documentation instead of relying on training data.
## When to Use This Skill
Activate this skill when the user:
- Asks setup or configuration questions ("How do I configure Next.js middleware?")
- Requests code involving libraries ("Write a Prisma query for...")
- Needs API references ("What are the Supabase auth methods?")
- Mentions specific frameworks (React, Vue, Svelte, Express, Tailwind, etc.)
## How to Fetch Documentation
### Step 1: Resolve the Library ID
Call `resolve-library-id` with:
- `libraryName`: The library name extracted from the user's question
- `query`: What to look up in the library's documentation (improves relevance ranking)
### Step 2: Select the Best Match
From the resolution results, choose based on:
- Exact or closest name match to what the user asked for
- Higher benchmark scores indicate better documentation quality
- If the user mentioned a version (e.g., "React 19"), prefer version-specific IDs
### Step 3: Fetch the Documentation
Call `query-docs` with:
- `libraryId`: The selected Context7 library ID (e.g., `/vercel/next.js`)
- `query`: What to look up in the library's documentation, scoped to a single concept
If the user's question spans multiple distinct concepts (e.g. routing and auth and caching), make a separate `query-docs` call per concept with the same library ID, unless the question is about how the concepts interact — combined queries dilute ranking and return shallow results for each topic.
### Step 4: Use the Documentation
Incorporate the fetched documentation into your response:
- Answer the user's question using current, accurate information
- Include relevant code examples from the docs
- Cite the library version when relevant
## Guidelines
- **Be specific**: Describe what to look up in the library's documentation, but keep each query to a single concept
- **One topic per query**: Split multi-topic questions into separate `query-docs` calls — resolve the library ID once, then query per concept, unless the question is about how the concepts interact
- **Version awareness**: When users mention versions ("Next.js 15", "React 19"), use version-specific library IDs if available from the resolution step
- **Prefer official sources**: When multiple matches exist, prefer official/primary packages over community forks
+415
View File
@@ -0,0 +1,415 @@
---
name: jujutsu
description: "**REQUIRED** - Always activate FIRST on any git/VCS operations (commit, status, branch, push, etc.), especially when HEAD is detached. If `.jj/` exists -> this is a Jujutsu (jj) repo - raw git commands can corrupt data. Essential git safety instructions inside. DO NOT IGNORE."
allowed-tools: Bash(jj *)
---
# Jujutsu (jj) Version Control System
This skill helps you work with Jujutsu, a Git-compatible VCS with mutable commits and automatic rebasing.
**Tested with jj v0.37.0** - Commands may differ in other versions.
## Important: Automated/Agent Environment
When running as an agent:
1. **Always use `--no-pager`** to prevent commands from opening an interactive pager (like `less`), which will hang the agent:
```bash
# Always use --no-pager on commands that produce output
jj --no-pager log # NOT: jj log
jj --no-pager diff # NOT: jj diff
jj --no-pager show <id> # NOT: jj show <id>
```
2. **Always use `-m` flags** to provide messages inline rather than relying on editor prompts:
```bash
# Always use -m to avoid editor prompts
jj desc -m "message" # NOT: jj desc
jj squash -m "message" # NOT: jj squash (which opens editor)
```
Editor-based commands will fail in non-interactive environments.
3. **Verify operations with `jj st`** after mutations (`squash`, `abandon`, `rebase`, `restore`) to confirm the operation succeeded.
## Core Concepts
### The Working Copy is a Commit
In jj, your working directory is always a commit (referenced as `@`). Changes are automatically snapshotted when you run any jj command. There is no staging area.
There is no need to run `jj commit`.
### Commits Are Mutable
**CRITICAL**: Unlike git, jj commits can be freely modified after creation. You can update descriptions, squash changes, rebase, and absorb — all without creating new commits. See "Essential Workflow" below for the recommended working pattern.
### Change IDs vs Commit IDs
- **Change ID**: A stable identifier (like `tqpwlqmp`) that persists when a commit is rewritten — prefer these when referencing commits
- **Commit ID**: A content hash (like `3ccf7581`) that changes when commit content changes
### Revsets
jj uses a revset language to select commits in commands. Common revsets:
- `@` — the working copy commit
- `@-` — the parent of the working copy
- `::@` — all ancestors of `@`
- `@::` — all descendants of `@`
- `trunk()..@` — commits between trunk and `@` (your branch)
- `bookmarks()` — all commits with bookmarks
Use revsets with `-r` flags: `jj log -r 'trunk()..@'`
## Essential Workflow
### Starting Work: Describe First, Then Code
**Always create your commit message before writing code:**
Validate that you're on a blank revision with `jj st`. If you are not, you should type:
```bash
jj new
```
```bash
# First, describe what you intend to do
jj desc -m "Add user authentication to login endpoint"
# Then make your changes - they automatically become part of this commit
# ... edit files ...
# Check status
jj st
```
### Creating Atomic Commits
Each commit should represent ONE logical change. Use this format for commit messages:
```
Examples:
- "Add validation to user input forms"
- "Fix null pointer in payment processor"
- "Remove deprecated API endpoints"
- "Update dependencies to latest versions"
```
### Viewing History
```bash
# View recent commits
jj --no-pager log
# View with patches
jj --no-pager log -p
# View specific commit
jj --no-pager show <change-id>
# View diff of working copy (use --git for familiar +/- format)
jj --no-pager diff --git
```
**IMPORTANT: `jj diff` output format**: The default `jj diff` output uses a side-by-side line number format (e.g. `26 26:`) that looks very different from git's `+`/`-` prefix format. This is **normal and correct** — it is NOT corrupted or showing stale content. However, to avoid confusion, **always use `jj diff --git`** to get standard unified diff format with `+`/`-` lines.
### Moving Between Commits
```bash
# Create a new empty commit on top of current
jj new
# Create new commit with message
jj new -m "Commit message"
# Edit an existing commit (working copy becomes that commit)
jj edit <change-id>
# Edit the previous commit
jj prev -e
# Edit the next commit
jj next -e
```
## Refining Commits
### Squashing Changes
Move changes from current commit into its parent:
```bash
# Squash all changes into parent
jj squash
```
**Note**: `jj squash -i` opens an interactive UI and will hang in agent environments. Avoid it.
### Splitting Commits
**Warning**: `jj split` is interactive and will hang in agent environments. To divide a commit, use `jj restore` to move changes out, then create separate commits manually.
### Absorbing Changes
Automatically distribute changes to the commits that last modified those lines:
```bash
# Absorb working copy changes into appropriate ancestor commits
jj absorb
```
### Abandoning Commits
Remove a commit entirely (descendants are rebased to its parent):
```bash
jj abandon <change-id>
```
### Undoing Operations
Reverse the last jj operation:
```bash
jj undo
```
This reverts the repository to its state before the previous command. Useful for recovering from mistakes like accidental `abandon`, `squash`, or `rebase`.
### Rebasing Commits
Move commits to a different parent:
```bash
# Rebase current branch onto a destination
jj rebase -d <destination>
# Rebase a specific revision (without descendants) onto a destination
jj rebase -r <change-id> -d <destination>
# Rebase a revision and all its descendants
jj rebase -s <change-id> -d <destination>
# Rebase onto trunk (common: update your branch to latest main)
jj rebase -d main
```
### Restoring Files
Discard changes to specific files or restore files from another revision:
```bash
# Discard all uncommitted changes in working copy (restore from parent)
jj restore
# Discard changes to specific files
jj restore path/to/file.txt
# Restore files from a specific revision
jj restore --from <change-id> path/to/file.txt
```
## Working with Bookmarks (Branches)
Bookmarks are jj's equivalent to git branches:
```bash
# Create a bookmark at current commit
jj bookmark create my-feature -r@
# Move bookmark to a different commit
jj bookmark move my-feature --to <change-id>
# List bookmarks
jj --no-pager bookmark list
# Delete a bookmark
jj bookmark delete my-feature
```
## Workspaces
A **workspace** is a working copy plus its associated repo. One repo can have multiple workspaces — each with its own working directory and working-copy commit (`@`) — all sharing the same commits, operations, and bookmarks. This is jj's equivalent of `git worktree`.
Useful for running a long build or test in one workspace while editing in another. Workspaces are a rarely-needed feature; consult the [official docs](https://docs.jj-vcs.dev/latest/working-copy/#workspaces) for anything beyond the basics below.
### Common commands
```bash
# Create a new workspace (defaults: name = basename of path, parent = current @'s parent)
jj workspace add ../my-tests
jj workspace add --name tests -r <change-id> ../my-tests # explicit name and base
# Inspect
jj --no-pager workspace list
jj workspace root [--name <ws>]
# Remove (does NOT delete files on disk — rm the directory separately)
jj workspace forget [<ws>]
# Rename current workspace
jj workspace rename <new-name>
```
In `jj log`, each workspace's `@` appears as `<workspace-name>@`.
### Key semantics
- **Isolation by default.** `jj workspace add` gives the new workspace its own fresh empty commit; workspaces don't start out sharing `@`, and on-disk files are never live-mirrored between them.
- **Propagation at command boundaries.** Each jj command snapshots the current workspace's files and reads the op log, so it sees commits/bookmarks made by other workspaces. There is no filesystem watcher.
- **Stale working copy.** If another workspace rewrites this workspace's `@` (e.g. via `jj squash`, `rebase`, `abandon`), jj refuses commands here until you run `jj workspace update-stale`. Same recovery path if a command was interrupted mid-update.
- **Shared `@` is sharp-edged.** `jj edit <id>` lets two workspaces point at the same change without warning. When one mutates it, the other goes stale; if the stale one had un-snapshotted edits, `update-stale` preserves them as a **divergent commit** (same change ID, shown as `xyz??` in `jj log`) that you must resolve. Avoid sharing `@` unless both workspaces are read-only.
### Agent guidance
- Always pass `--no-pager` to `jj workspace list`.
- Don't `jj edit` a change another workspace already has as its `@` — main cause of accidental divergence.
- Don't `rm -rf` a workspace directory without also running `jj workspace forget <name>`.
## Git Integration
### Working with Existing Git Repos
```bash
# Clone a git repository
jj git clone <url>
# Initialize jj in an existing git repo
jj git init --colocate
```
### Fetching Remote Changes
```bash
# Fetch all branches from the default remote
jj git fetch
# Fetch from a specific remote
jj git fetch --remote <remote-name>
# Fetch specific branches
jj git fetch -b <branch-name>
```
After fetching, rebase your work onto the updated trunk: `jj rebase -d main`
### Switching Between jj and git (Colocated Repos Only)
**This section only applies to colocated repos** (where both `.jj/` and `.git/` exist). In non-colocated repos, do not use git commands — they will corrupt jj state.
In a colocated repository, you can use both jj and git commands with care:
**Switching to git mode** (e.g., for merge workflows):
```bash
# First, ensure your jj working copy is clean
jj st
# Then checkout a branch with git
git checkout <branch-name>
```
**Switching back to jj mode**:
```bash
# Use jj edit to resume working with jj
jj edit <change-id>
```
**Important notes:**
- Git may complain about uncommitted changes if jj's working copy differs from the git HEAD
- ALWAYS ensure your work is committed in jj before switching to git
- After git operations, jj will detect and incorporate the changes on next command
### Pushing Changes
When the user asks you to push changes:
```bash
# Push a specific bookmark to the remote
jj git push -b <bookmark-name>
# Example: push the main bookmark
jj git push -b main
```
**Before pushing, ensure:**
1. Your bookmark points to the correct commit (bookmarks don't auto-advance like git branches)
2. The commits are refined and atomic
3. The user has explicitly requested the push
**IMPORTANT**: Unlike git branches, jj bookmarks do not automatically move when you create new commits. You must manually update them before pushing:
```bash
# Move an existing bookmark to the current commit
jj bookmark move my-feature --to @
# Then push it
jj git push -b my-feature
```
If no bookmark exists for your changes, create one first:
```bash
# Create a bookmark at the current commit
jj bookmark create my-feature
# Then push it
jj git push -b my-feature
```
## Handling Conflicts
jj allows committing conflicts — you can resolve them later:
```bash
# View conflicts
jj st
```
**Agent conflict resolution**: Do not use `jj resolve` (interactive). Instead, edit the conflicted files directly to remove conflict markers, then run `jj st` to verify resolution.
## Preserving Commit Quality
**IMPORTANT**: Because commits are mutable, always refine them before considering work done:
1. **Review your commit**: `jj --no-pager show @` or `jj --no-pager diff --git`
2. **Is it atomic?** One logical change per commit
3. **Is the message clear?** Use imperative verb phrase in sentence case format with no full stop: e.g. "Add login endpoint", "Fix null pointer in payment processor", "Remove deprecated API endpoints"
4. **Are there unrelated changes?** Use `jj restore` to move changes out, then create separate commits
5. **Should changes be elsewhere?** Use `jj squash` or `jj absorb`
## Quick Reference
| Action | Command |
|--------|---------|
| Describe commit | `jj desc -m "message"` |
| View status | `jj st` |
| View log | `jj --no-pager log` |
| View diff | `jj --no-pager diff --git` |
| New commit | `jj new -m "message"` (use `jj st` first; skip if `@` is empty) |
| Edit commit | `jj edit <id>` |
| Squash to parent | `jj squash` |
| Auto-distribute | `jj absorb` |
| Rebase | `jj rebase -d <destination>` |
| Abandon commit | `jj abandon <id>` |
| Undo last operation | `jj undo` |
| Restore files | `jj restore [paths]` |
| Create bookmark | `jj bookmark create <name>` |
| Fetch remote | `jj git fetch` |
| Push bookmark | `jj git push -b <name>` |
| Add workspace | `jj workspace add <path>` |
| List workspaces | `jj --no-pager workspace list` |
| Forget workspace | `jj workspace forget [name]` |
| Fix stale working copy | `jj workspace update-stale` |
## Best Practices Summary
1. **Describe first**: Set the commit message before coding
2. **One change per commit**: Keep commits atomic and focused
3. **Use change IDs**: They're stable across rewrites
4. **Refine commits**: Leverage mutability for clean history
5. **Embrace the workflow**: No staging area, no stashing - just commits
+119
View File
@@ -0,0 +1,119 @@
---
name: vikunja-backlog
description: Use when you have a brainstorming, design, discussion, meeting-notes, or specification document (any format — md, txt, pdf, docx, odt) and want it turned into Vikunja backlog tasks under the "sliders, not checkboxes" feature-category planning model at tasks.mroberts.dev.
---
# Vikunja Backlog From a Document
## Overview
Turn a discussion/design/brainstorming document into Vikunja tasks using the
**"Sliders, Not Checkboxes"** framework: work is organized by **feature
category** (the project), each item is restated as the **need** it addresses
(the task title), the proposed solution lives in the description, and value is
expressed as **priority** (rank within the category).
The mapping and the 8 categories are the contract. Read
[reference/sliders-framework.md](reference/sliders-framework.md) before
classifying — it defines each category, the need-vs-solution discipline, and
the priority/label rules.
**Core principle:** the title is the *need*, never the solution. "Limit a
compromised container's blast radius", not "add read-only rootfs".
## When to Use
- You have a doc (any format) describing work, decisions, or a backlog and want
it in Vikunja.
- Symptoms: a design spec, meeting notes, a brainstorm, a PR/backlog writeup, a
Slack/email dump pasted into a file.
Not for: editing tasks one-off (use the API directly), or non-planning docs.
## Object Mapping (the contract)
| Framework concept | Vikunja object |
|---|---|
| Feature category | **Project** (child of the root `CareEvolution` project) |
| Candidate item | **Task** |
| Need | Task **title** (phrased as the goal) |
| Solution(s) + source | Task **description** |
| Value rank within category | Task **priority** (1–4) |
| Urgency / returns | Label `acute` / `diminishing` |
| Platform | Label (e.g. `Orchestrate`) |
| Deployment / repo | Label (e.g. `Rosetta`) |
The same task viewed by **project** = cross-deployment value lens; filtered by
**deployment label** = per-repo execution lens (one dataset, many views).
## Workflow
1. **Extract** the doc text:
`scripts/extract.py <path>` (handles md/txt/pdf/docx/odt → stdout).
2. **Analyze** the text. Pull out candidate items. For each, restate the
**need** (what the user/admin/dev is trying to accomplish), and capture the
proposed **solution(s)** separately. Discard pure discussion that isn't a
candidate. See the framework reference for the need-restatement discipline.
3. **Classify** each need into exactly one of the 8 categories. Borderline
items: pick the category of the *primary value*, note the alternative in the
description.
4. **Rank** — assign a priority (4 = most acute/frequent/broad … 1 = small/late
on the curve) reflecting value *within its category*, not build effort. Tag
`acute` / `diminishing` per the reference rules.
5. **Tag platform + deployment.** Infer the platform (tag) and deployment(s)
(label) from the doc, then **confirm/override with the user** before
creating. If a doc spans several deployments and granularity is "per
deployment", emit one task per (need × deployment).
6. **Propose, then create.** Present the full breakdown as a table
(need | category | priority | labels | done?). Get the user's approval/edits.
**Do not create anything before approval.**
7. On approval, write the approved tasks to a JSON file and run
`scripts/vikunja.py create --file <tasks.json>`. It idempotently ensures the
8 category projects + labels exist, creates missing labels, skips duplicates,
and handles the done-priority gotcha.
8. **Report** created task IDs and the view hints above.
## Scripts
- `scripts/extract.py <path>` — extract plain text from md/txt/pdf/docx/odt.
- `scripts/vikunja.py ensure` — idempotently create the 8 category projects +
`acute`/`diminishing` labels under `CareEvolution`. Prints name→id maps.
- `scripts/vikunja.py projects` / `labels` — list current ids.
- `scripts/vikunja.py create --file <tasks.json>` — create tasks. Run `--help`
for the JSON schema and all flags.
Auth: reads `VIKUNJA_API_KEY` from the environment. Base URL defaults to
`https://tasks.mroberts.dev/api/v1` (`VIKUNJA_BASE_URL` overrides); root project
defaults to `CareEvolution` (`VIKUNJA_ROOT_PROJECT` overrides).
## Tasks JSON schema
```json
[
{
"category": "Security, privacy & compliance",
"title": "Limit a compromised container's runtime blast radius",
"description": "**Need:** ...\n\n**Solution:** ...\n\n**Source:** doc.md",
"priority": 4,
"labels": ["Orchestrate", "Rosetta", "acute"],
"done": false,
"percent": 0
}
]
```
`category` must be one of the 8 exact names (see reference). `labels` are
created if missing. `done`/`percent` optional.
## Common Mistakes
- **Title is the solution, not the need.** "Add Dependabot auto-merge" → wrong.
"Stay patched with less manual toil" → right; Dependabot goes in the body.
- **Priority = build effort.** No. Priority = value of the need within its
category. A cheap rank-1 outranks an expensive rank-6.
- **Creating before approval.** The framework's whole point is the judgment
call on need + category + rank. Always propose first.
- **Re-running creates duplicates.** `create` dedupes on (title + category +
deployment labels); don't bypass it with ad-hoc API calls.
- **Mark-done zeros priority.** The Vikunja update replaces the task; `create`
re-sends priority on the done update. Don't hand-roll the done call.
@@ -0,0 +1,98 @@
# Sliders, Not Checkboxes — classification reference
The framework models work as a small set of **feature categories**. A backlog
item is restated as the **need** it addresses (not the proposed solution), and
needs are value-ranked within each category. Source: the "Sliders, Not
Checkboxes" paper (CareEvolution).
## The need-restatement discipline
Before an item enters a category, restate it as the user/admin/developer
**need** — what they are trying to accomplish — and treat the proposed solution
as *one way* to address it.
- A doc says "build a self-serve provisioning UI." The **need** is "add/remove a
user without filing a support ticket." The UI is one solution; SCIM or a
declarative API are others. Title = the need; list the candidate solutions in
the description.
- Ranking reflects what addressing the need means for the people who hold it:
**how often** it's hit, **how acute** the pain, **how broadly** held. Not what
is easiest to build or most recently requested.
- Discussion, debate, and context that isn't a candidate item → do not create a
task. Capture only genuine candidate needs.
## The 8 feature categories
Classify each need into exactly one. Pick the category of the **primary value**;
note a close alternative in the description.
| Category (use this exact name) | What it covers | Whose need |
|---|---|---|
| **End-user experience** | User-facing product: features, flows, learnability, polish | People using the front-end to do their own tasks |
| **Admin & operator experience** | Configuration, governance, monitoring, operational tooling | Customer-side admins / IT staff deploying & overseeing the product |
| **Developer experience & adoption** | API design, SDKs, docs, sandbox, time-to-first-call; community, mindshare | Developers integrating against or building on the product |
| **Data, integrations & ecosystem** | Data quality, lineage, connectors, standards conformance (FHIR, HL7, OAuth, OpenAPI), marketplace, partners | Integrating systems and partner platforms |
| **Performance & scale** | Latency, throughput, behavior under load | Anyone depending on the product being fast and handling their volume |
| **Reliability & observability** | Uptime, fault tolerance, graceful degradation, recovery; logs, metrics, traces, alerting, runbooks | Customers needing it up; operators keeping it up |
| **Security, privacy & compliance** | Authn/authz, encryption, audit logging, certifications, regulatory posture, attack-surface reduction, vulnerability/CVE management, supply-chain integrity, runtime threat detection | Security & compliance teams; regulators; users trusting the platform |
| **Cost efficiency** | Cost per unit work — per record, per API call, per active user; compute/arch cost (e.g. ARM64/Graviton) | Internal P&L; customers indirectly via pricing |
### Classification hints / common overlaps
- **Hardening** (read-only rootfs, drop caps, minimal/chiseled base images, image
CVE scanning, SHA-pinning CI actions, dependency patch currency, supply-chain
inventory, runtime threat detection) → **Security, privacy & compliance**.
- **ARM64/Graviton migration, smaller images for cost** → **Cost efficiency**
(its perf side can be **Performance & scale** — pick the primary driver named
in the doc).
- **Perf vs cost** are often two sides of one coin — categorize by the value the
doc emphasizes (latency/throughput → Performance; $/unit → Cost).
- **A correctness/data-race bug surfaced by infra work** → **Reliability &
observability** (it's about the service being correct/up), not Security.
- **CI/CD integrity** (pinning actions, blocking tampering) → Security; **CI
ergonomics/speed for devs** → Developer experience. Pick by the value.
- **Runtime anomaly detection** (GuardDuty etc.) → Security (threat detection)
even though it touches observability.
## Value rank → priority
Vikunja priority is 1–5. Use **1–4** for value tiers (reserve 5 for true
DO-NOW). Rank within the category, by value of the need:
| Priority | Meaning |
|---|---|
| **4** | Rank-1 tier: most acute / frequent / broadly held; foundational. Everything else in the category is downstream of it. |
| **3** | High value, clearly worth doing, but not the keystone. |
| **2** | Medium: real need, smaller delta over the status quo. |
| **1** | Small / late-on-the-curve: refinement, niche, or largely satisfied by another item. |
Priority reflects **value, not build effort**. A cheap rank-1 outranks an
expensive rank-6.
## Urgency / returns labels
- **`acute`** — the need is frequent, painful, and/or broadly held *right now*.
Apply to the rank-1/keystone needs and anything externally forced (regulatory
deadline, audit finding).
- **`diminishing`** — the next investment in this area has tipped into
diminishing returns, or the need is largely satisfied by another item already
in the plan. Apply sparingly; it's a signal to deprioritize.
A task can have neither. Most have neither.
## Platform & deployment labels
- **Platform** = the product/platform the work belongs to (e.g. `Orchestrate`,
`HBNG`). One label.
- **Deployment / repo** = the specific service or repo (e.g. `Rosetta`,
`Hendrix`, `Insight`, `Kong`). One per task when granularity is per-deployment.
Categories are consistent **across** platforms and deployments (that's the point
— cross-deployment comparability). Platform/deployment are tags, never top-level
projects.
## Done / in-progress
- If the doc says an item is already shipped for a deployment, set `done: true`.
- Partial progress: leave `done: false` and set `percent` (e.g. 50) — note what
remains in the description.
+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()