v0.1 wave 3 (steps 9+10): autonomous patch loop + production recipes

Step 9 — autonomous patch loop:
- patcher.py: clawdforge session → unified diff → worktree apply → verify recipe → push branch → open Gitea PR
- migration 007: patch_attempts (UNIQUE per finding+attempt, max 3 attempts)
- runner.py: post-parse hook fires patcher.maybe_draft_for_job when notify.auto_patch=true
- server.py: POST /jobs/{id}/patches, GET /patches, GET /patches/{id}
- digest.py: patch-drafted lines + open-follow-up count via Gitea PR state check
- mcp: crafting_table_draft_patch stub replaced with real implementation
- tests/test_patcher.py + tests/test_patches_api.py: 27 new tests

No auto-merge — patches stop at PR-open. Cobb merges.

Step 10 — production recipes:
- examples/recipes/clawdforge.json: 14 subprojects across all SDKs, audit nightly
- examples/recipes/cauldron.json: single Flask subproject, audit nightly
- examples/recipes/tradecraft.json: nightly audit, auto_patch=false (manual review)
- examples/register-all.sh: bulk-register helper with GITEA_TOKEN substitution
- README "Autonomous patch loop" + "First production recipes" sections

Tests: server 116→143, mcp 65→67. All green.

Spec: memory/spec-crafting-table.md
This commit is contained in:
Kayos 2026-04-29 09:04:48 -07:00
parent ecb9d76e6d
commit 4eab869df0
17 changed files with 2752 additions and 78 deletions

View file

@ -0,0 +1,19 @@
{
"name": "cauldron",
"git_url": "http://kayos:REPLACE_WITH_GITEA_TOKEN@192.168.0.5:3001/Sulkta-Coop/cauldron.git",
"default_branch": "main",
"languages": ["python"],
"subprojects": [
{
"path": ".",
"language": "python",
"build": "pip install -e .[test]",
"test": "pytest tests/",
"lint": "ruff check .",
"audit": "pip-audit",
"timeout_secs": 600
}
],
"schedule": {"audit": "0 2 * * *", "test": "0 */6 * * *"},
"notify": {"email": ["cobb@sulkta.com"], "on": ["audit_fail", "test_fail", "cve_found", "patch_drafted"], "auto_patch": true}
}

View file

@ -0,0 +1,24 @@
{
"name": "clawdforge",
"git_url": "http://kayos:REPLACE_WITH_GITEA_TOKEN@192.168.0.5:3001/Sulkta-Coop/clawdforge.git",
"default_branch": "main",
"languages": ["python", "rust", "go", "ruby", "php", "java", "csharp", "swift", "kotlin", "c", "cpp", "bash", "typescript", "mcp"],
"subprojects": [
{"path": "clients/python", "language": "python", "build": "pip install -e .[test]", "test": "pytest tests/", "lint": "ruff check . && mypy --strict src/", "audit": "pip-audit", "timeout_secs": 600},
{"path": "clients/rust", "language": "rust", "build": "cargo build --release", "test": "cargo test --all", "lint": "cargo clippy --all-targets -- -D warnings && cargo fmt --check", "audit": "cargo audit", "timeout_secs": 1200},
{"path": "clients/go", "language": "go", "build": "go build ./...", "test": "go test ./...", "lint": "go vet ./...", "audit": "govulncheck ./...", "timeout_secs": 600},
{"path": "clients/typescript", "language": "typescript", "build": "npm install --no-audit", "test": "node --test --import tsx tests/*.test.ts", "lint": "npx tsc --noEmit", "audit": "npm audit", "timeout_secs": 600},
{"path": "clients/ruby", "language": "ruby", "build": "bundle install", "test": "bundle exec rake test", "lint": null, "audit": "bundler-audit", "timeout_secs": 600},
{"path": "clients/php", "language": "php", "build": "composer install", "test": "vendor/bin/phpunit", "lint": null, "audit": "composer audit", "timeout_secs": 600},
{"path": "clients/java", "language": "java", "build": "mvn package -DskipTests", "test": "mvn test", "lint": "mvn javadoc:javadoc -Dquiet=false", "audit": null, "timeout_secs": 1200},
{"path": "clients/csharp", "language": "csharp", "build": "dotnet build -c Release", "test": "dotnet test -c Release", "lint": null, "audit": "dotnet list package --vulnerable --include-transitive", "timeout_secs": 900},
{"path": "clients/c", "language": "c", "build": "cmake -S . -B build && cmake --build build", "test": "ctest --test-dir build --output-on-failure", "lint": null, "audit": null, "timeout_secs": 900},
{"path": "clients/cpp", "language": "cpp", "build": "cmake -S . -B build && cmake --build build", "test": "ctest --test-dir build --output-on-failure", "lint": null, "audit": null, "timeout_secs": 900},
{"path": "clients/kotlin", "language": "kotlin", "build": "./gradlew --no-daemon build", "test": "./gradlew --no-daemon test", "lint": null, "audit": null, "timeout_secs": 1800},
{"path": "clients/bash", "language": "bash", "build": null, "test": "bash test/run.sh", "lint": "shellcheck cf", "audit": null, "timeout_secs": 300},
{"path": "clients/mcp", "language": "python", "build": "pip install -e .", "test": "pytest tests/", "lint": null, "audit": null, "timeout_secs": 300},
{"path": ".", "language": "python", "build": "pip install -e .", "test": "pytest tests/", "lint": null, "audit": null, "timeout_secs": 600}
],
"schedule": {"audit": "0 2 * * *", "test": "0 8 * * *"},
"notify": {"email": ["cobb@sulkta.com"], "on": ["audit_fail", "test_fail", "cve_found", "patch_drafted"], "auto_patch": true}
}

View file

@ -0,0 +1,19 @@
{
"name": "tradecraft",
"git_url": "http://kayos:REPLACE_WITH_GITEA_TOKEN@192.168.0.5:3001/TradeCraft/tradecraft.git",
"default_branch": "main",
"languages": ["python"],
"subprojects": [
{
"path": ".",
"language": "python",
"build": "pip install -e .",
"test": "pytest tests/",
"lint": "ruff check .",
"audit": "pip-audit",
"timeout_secs": 900
}
],
"schedule": {"audit": "0 2 * * *"},
"notify": {"email": ["cobb@sulkta.com"], "on": ["audit_fail", "cve_found"], "auto_patch": false}
}

48
examples/register-all.sh Executable file
View file

@ -0,0 +1,48 @@
#!/bin/bash
# Register all example recipes against a running crafting-table instance.
#
# Reads the bearer token from $CRAFTING_TABLE_TOKEN, falling back to
# /data/admin-bearer.txt (the path inside the container) if unset. The
# admin bearer file is also bind-mounted at
# /mnt/user/appdata/crafting-table/data/admin-bearer.txt on the Lucy host
# — that's the recommended source on the host side.
#
# IMPORTANT: the recipe JSON files in recipes/ ship with a placeholder
# git_url containing "REPLACE_WITH_GITEA_TOKEN". This script substitutes
# $GITEA_TOKEN into each recipe before posting; commit-time the real
# token never lives on disk.
set -euo pipefail
BASE_URL=${CRAFTING_TABLE_URL:-http://192.168.0.5:8810}
TOKEN=${CRAFTING_TABLE_TOKEN:-$(cat /data/admin-bearer.txt 2>/dev/null || echo "")}
GITEA_TOKEN=${GITEA_TOKEN:-}
if [ -z "$TOKEN" ]; then
echo "no crafting-table token (set CRAFTING_TABLE_TOKEN or ensure /data/admin-bearer.txt exists)" >&2
exit 1
fi
if [ -z "$GITEA_TOKEN" ]; then
echo "no Gitea token (set GITEA_TOKEN to substitute into recipe git_url)" >&2
exit 1
fi
DIR="$(dirname "$0")/recipes"
for recipe in "$DIR"/*.json; do
name="$(basename "$recipe" .json)"
echo "registering $name from $recipe..."
body="$(sed "s|REPLACE_WITH_GITEA_TOKEN|$GITEA_TOKEN|g" "$recipe")"
code=$(printf '%s' "$body" | curl -s -o /tmp/register-resp.json \
-w "%{http_code}" \
-X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-binary @- \
"$BASE_URL/projects" || true)
if [ "$code" = "200" ]; then
echo " ok"
elif [ "$code" = "409" ]; then
echo " already exists — use PUT /projects/$name to update"
else
echo " FAILED ($code): $(cat /tmp/register-resp.json 2>/dev/null || echo no-body)"
fi
done