Commit graph

27 commits

Author SHA1 Message Date
1ef50307ac fix go-install verification in Dockerfile
Latent bug: the post-loop check used `command -v` to verify
govulncheck and staticcheck installed. `command -v` only walks
PATH, but at this layer PATH does NOT include $GOPATH/bin
(/home/crafter/go/bin) — that's only added in the canonical
final PATH at the bottom of the Dockerfile (line 314). At
runtime the binaries work fine via the bottom PATH; only the
build-time verify was broken.

The bug was masked by stale Docker layer caching from earlier
Dockerfile shapes. Adding the new Nix layer above this step
invalidated the cache and surfaced it.

Switch to direct binary path checks (test -x \"\$GOPATH/bin/...\")
which work regardless of PATH state at the layer.
2026-05-06 17:05:37 -07:00
b0490a8c02 add Nix toolchain + bump Go to 1.25.9
Two coupled changes:

1. Add a single-user Nix install at section 19.5 so the container can
   `nix develop` / `nix run` / `nix build` for the Cardano smart-
   contract toolchain stack (Plutarch, plutus-core, Liqwid Agora's
   `agora-scripts` exporter — all ship as IOG haskell-nix flakes
   with pinned GHC). Without Nix, building any of those is a manual-
   version-pinning fight.

   Single-user mode (no daemon), sandbox=false (containers can't nest
   sandboxes cleanly), flakes + nix-command experimental features
   enabled. /nix is owned by `crafter` and bind-mounted from
   /mnt/user/appdata/crafting-table/nix in compose so the multi-GB
   haskell-nix downloads survive container rebuilds.

2. Bump GO_VERSION 1.22.10 → 1.25.9. govulncheck@latest (v1.3.0) and
   staticcheck@latest (v0.7.0) both now require Go ≥ 1.25 — building
   with 1.22 hits "requires go >= 1.25.0" and the per-step retry loop
   exhausts. Go's auto-toolchain-switch tries to download 1.25.9 on
   the fly but staticcheck's parent build then runs in 1.22 and
   re-fails. Pinning to 1.25.9 (current Go release) sidesteps the
   wedge.

PATH bump: prepend /home/crafter/.nix-profile/bin so nix-installed
binaries (cabal, ghc inside dev shells, cardano-cli, etc) take
precedence over system tooling without per-recipe prefixing.

Build invocation unchanged — nothing required at the docker run /
docker compose layer beyond the new /nix bind mount in compose.yml.
2026-05-06 15:08:01 -07:00
d3babae46d Dockerfile: remove dead PATH-clobber from GOPATH-fix; final clean PATH at end is single source of truth 2026-04-29 16:07:40 -07:00
3273d66003 patcher: robust extract_diff_json — handles 5 model-output shapes
The 4 patcher-fired-but-malformed_response failures showed extract_diff_json
was too strict: it required {"diff": "..."} as the top-level JSON shape
with at most 1 brace nesting depth (regex-based). Real model output
varies more.

Now handles:
1. Bare JSON {"diff", "explanation", "confidence"}
2. Fenced JSON: ```json {…} ```
3. Fenced diff + prose: ```diff …unified diff… ``` + loose explanation
4. Bare unified diff (no JSON wrapper, no fence)
5. JSON with deeply-nested {} inside the diff string (struct literals,
   function bodies)

Fixes:
- Replaced regex-based balanced-{} matcher (capped at depth 1) with a
  string-aware depth-tracking generator that handles arbitrary nesting
  + skips brace chars inside JSON string literals
- Walk all fenced blocks not just the first; recognize ```diff and
  ```patch language tags
- Fall back to fenced-diff-with-prose construction when no JSON form
  matches — synthetic payload with surrounding text as explanation
- Final fallback for bare unified diffs (no fence, no wrapper) using a
  simple line-prefix detector
- Normalize alternate keys (patch, content, diff_text → diff)
- Always set confidence (defaults to medium when absent, low for bare
  diffs that have no model commentary)

Tests: 16 → 20 (5 new shape coverage tests). All green.
2026-04-29 15:37:24 -07:00
80c4eebf3b Dockerfile: --force-reinstall on app pip install (otherwise click stays in /root/.local from pipx bootstrap and uvicorn can't see it) 2026-04-29 15:21:11 -07:00
70d6df2414 requirements: explicit pin uvicorn standard extras (click etc) to survive build cache invalidation 2026-04-29 21:49:35 +00:00
327b072545 Dockerfile: use python3 -m pipx (PATH-free invocation) for pipx layer 2026-04-29 14:43:30 -07:00
569691a555 Dockerfile: skip cargo-audit/cargo-deny install (both flaking); keep mypy types-requests fix 2026-04-29 14:42:40 -07:00
44535acdf8 Dockerfile: cargo-deny via prebuilt github release binary (cargo install too flaky) 2026-04-29 14:41:48 -07:00
64415348ce Dockerfile: drop cargo-audit (libgit2-sys broken); cargo-deny supersedes 2026-04-29 14:40:27 -07:00
e268986f87 Dockerfile: cargo-audit/cargo-deny to /usr/local + mypy type stubs
Two recipe-shape gaps caught by the all-SDK lint+audit dogfood:

1. `cargo install --root /caches/cargo cargo-audit cargo-deny` lost its
   binaries at runtime because /caches/cargo is volume-shadowed by the
   host bind mount. Fix: install with `--root /usr/local` so the bins
   land in /usr/local/bin (root-owned, not volume-shadowed). Required
   USER root briefly to write to /usr/local; reverts to crafter after.

2. `mypy --strict` against any project that imports requests/PyYAML/
   setuptools fails with "Library stubs not installed" exit 1 because
   pipx-installed mypy lives in its own venv and doesn't see the
   stubs. Fix: `pipx inject mypy types-requests types-PyYAML
   types-setuptools` so the stubs land in mypy's venv.
2026-04-29 14:20:53 -07:00
510915d3ec Dockerfile: clean final PATH at end (single source of truth)
The agent-generated Dockerfile accumulated PATH via 6+ layered ENV
PATH= statements, and my own GOPATH-fix edit (commit 6cd5990) wrote
a literal-expanded PATH that clobbered the swift/kotlin/gradle/bun/
cargo entries. Result: cargo unreachable from crafter user (caught
by the 14-SDK queue dogfood — exit 127 'Permission denied' on cargo
build).

Fix: a final ENV PATH= line right before the CMD that sets PATH to
a clean, comprehensive list of every toolchain bin. Overrides any
drift above. Includes:
- /home/crafter/.local/bin (pipx tools: ruff, mypy, pytest, pip-audit, uv, semgrep)
- /home/crafter/.composer/vendor/bin (phpstan, phpunit)
- /home/crafter/.local/share/gem/ruby/3.1.0/bin (bundler-audit, rubocop)
- /home/crafter/.bun/bin (bun)
- /home/crafter/go/bin (govulncheck, staticcheck)
- /home/crafter/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin (cargo, rustc, clippy, rustfmt)
- /caches/cargo/bin (cargo install artifacts; volume-mounted)
- /opt/swift/usr/bin (swift)
- /opt/kotlin/bin (kotlinc)
- /opt/gradle/bin (gradle)
- /usr/local/go/bin (go)
- system bins

Once this rebuild lands, the rust recipes can drop the per-recipe
PATH= prefix the workaround used.
2026-04-29 14:09:32 -07:00
3578c9321b workspace: explicit fetch right after bare clone (populates remote-tracking refs that --bare doesn't) 2026-04-29 13:58:28 -07:00
129d630391 workspace: per-project lock + fetch into remote-tracking refs
Two bugs caught by the first real concurrent dogfood (15 SDK build jobs
queued at once for clawdforge):

1. Concurrent fetch race — multiple jobs from same project all called
   `git fetch +refs/heads/*:refs/heads/*` simultaneously. Git refuses
   to fetch into a local branch ref that another worktree has checked
   out, so jobs after the first failed fast with:
       fatal: refusing to fetch into branch 'refs/heads/main' checked
       out at '/workspace/clawdforge/<other-job-id>'

2. Worktree-from-local-branch — `worktree add ... main` reserved the
   local branch ref to the worktree, blocking subsequent fetches even
   when the lock above wasn't held.

Fix:
- Per-project asyncio.Lock around the materialize() body (clone +
  fetch + worktree-add). Different projects still parallelize; same
  project serializes through the cache dir.
- Fetch into remote-tracking refs only:
    +refs/heads/*:refs/remotes/origin/*
  Local refs/heads/* are never written, so no worktree can hold them.
- worktree add uses `--detach origin/<branch>` so each worktree is at
  the remote-tracking ref. Multiple detached worktrees share the same
  remote ref without conflict.

The recipe itself runs outside the lock, so concurrency=4 still
parallelizes the actual build/test work — only the brief
materialize step (clone or fetch + worktree create) serializes.

Tests still 6/6 green on test_runner.py (uses _StubWorkspace, not the
real WorkspaceManager, so it's unaffected — the real exercise is the
live re-queue against fixed code).
2026-04-29 13:56:44 -07:00
61a9814b67 patcher: pin model='opus' on clawdforge sessions; CRAFTING_PATCHER_MODEL env override
Code-work prompts (read CVE/lint context, draft a unified diff that
verifies cleanly against the failing recipe) reward Opus's longer
context + careful reasoning. Cauldron-style high-frequency Sonnet calls
are unaffected — this only changes what crafting-table's patcher asks
clawdforge to use for its drafted-patch sessions.

Pairs with clawdforge dbbead2 which adds the optional `model` field on
POST /sessions and propagates ANTHROPIC_MODEL into the acpx subprocess
env on both create + turn.

Override knob: CRAFTING_PATCHER_MODEL env (e.g. "sonnet" if cost > quality).

Tests: 16/16 patcher tests still green.
2026-04-29 12:37:54 -07:00
a1b3c72c8f Dockerfile: retry go install up to 5x with backoff — DNS to 192.168.0.1:53 flakes intermittently for proxy.golang.org 2026-04-29 18:41:59 +00:00
6cd599079b Dockerfile: set GOPATH=/home/crafter/go BEFORE go install layer (was inheriting /root/go) 2026-04-29 18:06:02 +00:00
6c8b0528ab Dockerfile: pin shfmt URL to /releases/download/v3.10.0/ — /latest/download/ broke when shfmt latest moved past v3.10.0 2026-04-29 17:26:48 +00:00
101c8ec2e7 Dockerfile: SHELL [/bin/bash] before Swift layer — fixes ${var//pattern} bash-ism that dash chokes on 2026-04-29 17:16:43 +00:00
4eab869df0 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
2026-04-29 09:04:48 -07:00
ecb9d76e6d v0.1 wave 2B (step 7): MCP server — stdio JSON-RPC, 8 tools
- mcp/ subpackage: crafting-table-mcp (separate pip install)
- Self-contained requests-based HTTP client (mirrors clawdforge_mcp pattern)
- 8 tools: list_projects / register_project / run_audit / run_build / run_test / get_job / get_findings / draft_patch (stub)
- draft_patch is stubbed — full impl lands in wave 3 / step 9
- tests/: client + tool coverage, 401/404 surfacing
- Tools designed for LLM consumption; descriptions tuned for "when to use" guidance

Spec: memory/spec-crafting-table.md
2026-04-29 08:38:29 -07:00
d467b2f5be v0.1 wave 2A (steps 5+6): per-language parsers + findings extraction
- parsers/ package: rust / python / go / typescript / generic
- parser registry with language+recipe -> fallback resolution
- fingerprint hash (kind+file+line+code) for cross-run dedup
- runner.py post-exec hook: parse log, persist findings, count on job row
  (extraction runs before mark_job_finished so callers polling on terminal
  status see findings_count populated atomically)
- db.insert_finding / list_findings / increment_findings_count DAOs already
  shipped in wave 1; wired here
- GET /jobs/{id}/findings now returns real data (server route already
  shipped; was returning empty list because nothing populated the table)
- tests/test_parsers/: 6 modules + 11 fixtures (rust/python/go/typescript)
- tests/test_runner_findings.py: 3 integration tests
- README: tick steps 2-6, add Findings section

Suite: 108 passing (62 wave-1 + 46 new).
Spec: memory/spec-crafting-table.md
2026-04-29 08:36:16 -07:00
98306ca2e0 v0.1 wave 2C (step 8): email digest scheduler
- digest.py: DigestScheduler with daily 06:00 PT loop
- SmtpConfig env-driven (CRAFTING_SMTP_*)
- notify.on event filter respected per project
- GET /digests/{date} + POST /admin/digest/run-now (dry_run flag)
- migration 006: digest_runs (idempotency via UNIQUE(date, project_name))
- text + HTML email bodies; matches spec's worked example
- Server lifespan integration; gracefully disables if SMTP not configured
- tests/test_digest.py: 8 tests (aggregation / filter / smtp mock / idempotency / endpoint)

Patch-drafted line is a placeholder until wave 3 / step 9 ships.

Spec: memory/spec-crafting-table.md
2026-04-29 08:33:37 -07:00
2e16ec886d wave 1 wiring: Dockerfile API stage + compose API command + README quickstart
- Dockerfile: pip-install requirements.txt and copy crafting_table/ into
  /app, switch CMD from /bin/bash to uvicorn server (port 8810). pip lands
  in /usr/local/bin so the crafter user runs uvicorn without elevation.
- compose.yml: replace smoke.sh entrypoint with the API server command;
  bind 192.168.0.5:8810:8810 (LAN-only); switch named volumes to real
  Lucy appdata paths so /data + /workspace + /caches survive recreate.
  env_file marked optional so a fresh checkout boots without copying
  .env.example.
- README.md: tick steps 1-4 done, document API surface table, add
  curl-based quickstart (mint token → register project → kick off job →
  poll → stream log), and an architecture-notes section covering the
  recipe-immutability snapshot, process-group SIGTERM/SIGKILL escalation,
  WAL+single-writer trade-off, and the recipe-security stance.

Smoke remains runnable on demand:
  docker compose run --rm crafting-table /usr/local/bin/smoke.sh
2026-04-29 08:28:51 -07:00
0ec3a04676 v0.1 wave 1 (steps 2+3+4): SQLite ledger + FastAPI skeleton + async job runner
- db.py: migrations + DAOs for tokens / projects / jobs / findings (SQLite WAL)
- auth.py: SHA-256 bearer hashing + LAN-CIDR allowlist + admin/app token tiers
- models.py: Pydantic shapes (Project, Subproject, Schedule, Notify, Job, CreateJobRequest)
- server.py: FastAPI on port 8810; /healthz, /admin/tokens/*, /projects/*, /jobs, /jobs/{id}, /jobs/{id}/log, /jobs/{id}/findings
- runner.py: bounded asyncio pool, per-job timeout with process-group SIGTERM→SIGKILL escalation, orphaned-job recovery on boot
- workspace.py: bare-clone + worktree materialization, gc
- config.py: env-driven
- 62 tests across db / auth / projects / jobs / runner / e2e — all green

Cross-token project access returns 404 (not 403) — existence-leak guard.
Bearer tokens hashed at rest; admin token bootstrapped on first boot.
Recipe subprocess uses start_new_session=True so killpg targets the
whole process tree on timeout — child processes can't escape SIGKILL.
Pump task guarded with wait_for(2s) + cancel fallback against any
orphan that survives the group kill.

Wave 2 (parsers + findings extraction + MCP + email digest) pending.

Spec: memory/spec-crafting-table.md
2026-04-29 08:17:41 -07:00
4e668a79e1 v0.1 step 1: Dockerfile + per-language toolchain smoke
Monolith image with every toolchain in the spec:
- Python 3.12 + uv/ruff/mypy/pytest/pip-audit/semgrep
- Node 22 LTS + bun
- Go 1.22 + govulncheck/staticcheck
- Rust stable + cargo-audit/cargo-deny
- Ruby 3.x + bundler-audit
- PHP 8.x + composer/phpstan
- JDK 17 + 21 + Maven + Gradle
- .NET 8 SDK
- Swift 5.9.2
- Kotlin 1.9.25
- clang + cmake + valgrind + ASan/UBSan/TSan
- bash + shellcheck

smoke.sh proves each toolchain compiles + runs a hello-world.
compose.yml uses the existing 'sulkta' bridge network.

No API yet (steps 2-3); no MCP yet (step 7); no runner yet (step 4).
This is the foundation.

NOTE: docker build + smoke verification not yet run — sandbox doesn't
have docker. Needs `docker compose build && docker compose up` on Lucy
or any real Docker host before we trust the Dockerfile.

Spec: memory/spec-crafting-table.md
2026-04-29 07:29:53 -07:00
5bd1b1de7e Initial commit 2026-04-29 07:22:04 -07:00