Skip to content

Commit 5ab2802

Browse files
feat(contractiles): expand must|dust|trust from templates (follow-up to #77) (#78)
## Summary Follow-up to #77 which added new adjust/ + bust/ dirs. This expands the 3 remaining template-state files with echidnabot-specific comprehensive coverage. ## Diff | File | Before | After | |---|---|---| | contractiles/must/Mustfile | 35 lines (generic template) | 90 lines, 15 checks M001-M015 | | contractiles/dust/Dustfile | 29 lines (generic template) | 135 lines, 13 dust actions D001-D060 | | contractiles/trust/Trustfile.hs | 105 lines (template stubs) | 190 lines, 10 trust checks T001-T051 with driver | ## Compose with #77 After #77 + this PR, echidnabot has complete must|adjust|trust|bust|dust coverage paralleling echidna#214 (which uses a2ml format; this uses YAML/Haskell to match echidnabot's existing conventions). ## License method PLMP-1.0-or-later SPDX headers preserved exactly per [[feedback_no_automated_licence_edits]]. Header drift (echidnabot sole work → should be MPL-2.0) flagged for manual owner action; not swept. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ab73f01 commit 5ab2802

3 files changed

Lines changed: 405 additions & 130 deletions

File tree

contractiles/dust/Dustfile

Lines changed: 134 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,138 @@
11
# SPDX-License-Identifier: PLMP-1.0-or-later
2-
# Dustfile template - recovery and rollback semantics
2+
# Dustfile — fine-grained recovery for echidnabot
3+
#
4+
# Cleanup of small leftover state: stale branches, orphan caches,
5+
# /tmp leftovers, dropped DB connections, half-completed migrations.
6+
# Run with: just dust-check (informational) or just dust-<name> (action)
7+
# Error-code namespace: D###
38

49
version: 1
510

6-
recovery:
7-
logs:
8-
- name: decision-log
9-
path: logs/decisions.json
10-
reversible: true
11-
handler: "log-replay --reverse logs/decisions.json"
12-
13-
policy:
14-
- name: policy-rollback
15-
path: policy/policy.ncl
16-
rollback: "git checkout HEAD~1 -- policy/policy.ncl"
17-
notes: "Rollback policy to the previous known-good revision."
18-
19-
gateway:
20-
- name: bad-deployment
21-
event: "deploy.failure"
22-
undo: "kubectl rollout undo deployment/gateway"
23-
notes: "Undo a failed deployment while preserving audit logs."
24-
25-
dust-events:
26-
- name: decision-log-to-dust
27-
source: logs/decisions.json
28-
transform: "dustify --input logs/decisions.json --output logs/dust-events.json"
29-
notes: "Map gateway decision logs into reversible dust events."
11+
dust-cadence: weekly (cheap) ; before-release (thorough)
12+
13+
# ── Branch-Dust ────────────────────────────────────────────────────────
14+
15+
branches:
16+
- name: D001-stale-local-branches
17+
description: "Local branches whose remote-tracking ref is gone."
18+
severity: low
19+
probe: "git for-each-ref refs/heads --format '%(refname:short) %(upstream:track)' | grep -q '\\[gone\\]'"
20+
dust: "git fetch --prune; for b in $(git for-each-ref refs/heads --format='%(refname:short) %(upstream:track)' | awk '/gone/ {print $1}'); do gh pr list --repo hyperpolymath/echidnabot --head \"$b\" --state merged --json number --jq '.[0].number' | grep -q '^[0-9]' && git branch -D \"$b\"; done"
21+
reversible: yes
22+
23+
- name: D002-stale-remote-branches
24+
description: "Remote branches whose PR was squash-merged."
25+
severity: low
26+
probe: "gh pr list --repo hyperpolymath/echidnabot --state all --limit 50 --json mergedAt | jq '[.[] | select(.mergedAt != null)] | length' | grep -qE '^[1-9]'"
27+
dust: "per merged PR: gh api -X DELETE repos/hyperpolymath/echidnabot/git/refs/heads/<branch>"
28+
29+
# ── Worktree-Dust ──────────────────────────────────────────────────────
30+
31+
worktrees:
32+
- name: D010-dangling-git-worktrees
33+
description: "Worktrees pointing at nonexistent paths."
34+
severity: low
35+
probe: "git worktree list 2>&1 | grep -q '\\[prunable\\]'"
36+
dust: "git worktree prune -v"
37+
reversible: yes
38+
39+
- name: D011-tmp-agent-worktrees
40+
description: "/tmp/echidnabot-* leftover from prior agent runs."
41+
severity: low
42+
probe: "ls -d /tmp/echidnabot-* 2>/dev/null | wc -l | grep -qE '^[1-9]'"
43+
dust: "for d in /tmp/echidnabot-*; do git -C /home/hyperpolymath/developer/repos/echidnabot worktree remove --force \"$d\" 2>/dev/null || rm -rf \"$d\"; done"
44+
exclude: "active session work; the open #46 rsa branch worktree at /tmp/gitbot-tmp"
45+
46+
# ── Cache-Dust ─────────────────────────────────────────────────────────
47+
48+
caches:
49+
- name: D020-cargo-target-bloat
50+
description: "target/ directory exceeds 3GB."
51+
severity: low
52+
probe: "test -d target && du -sh target | awk '{print $1}' | grep -qE '^[3-9]G|^[1-9][0-9]+G$'"
53+
dust: "cargo clean"
54+
reversible: yes
55+
56+
- name: D021-podman-image-orphans
57+
description: "Dangling podman images."
58+
severity: low
59+
probe: "podman images --filter dangling=true --format '{{.ID}}' | wc -l | grep -qE '^[1-9]'"
60+
dust: "podman image prune -f"
61+
reversible: yes
62+
63+
# ── Process-Dust ───────────────────────────────────────────────────────
64+
65+
processes:
66+
- name: D030-orphan-pid-files
67+
description: "/tmp/*.pid pointing at dead PIDs."
68+
severity: low
69+
probe: "ls /tmp/echidnabot-*.pid 2>/dev/null | while read f; do kill -0 $(cat \"$f\") 2>/dev/null || echo \"$f\"; done | head -1"
70+
dust: "for f in /tmp/echidnabot-*.pid; do kill -0 $(cat \"$f\") 2>/dev/null || rm -f \"$f\" \"${f%.pid}.status\"; done"
71+
72+
# ── DB-Dust ────────────────────────────────────────────────────────────
73+
74+
database:
75+
- name: D040-stale-migrations
76+
description: "migrations/ contains files newer than the DB's last-applied (sqlx)."
77+
severity: medium
78+
probe: "test -d migrations && ls migrations/*.sql 2>/dev/null | wc -l | grep -qE '^[1-9]'"
79+
dust: "sqlx migrate run (apply pending) ; OR sqlx migrate revert (rollback last)"
80+
reversible: "yes (sqlx migrate revert)"
81+
82+
- name: D041-stale-sqlite-shm-wal
83+
description: "Stale -shm / -wal files alongside .db (sqlite ungraceful shutdown)."
84+
severity: low
85+
probe: "find . -name '*.db-shm' -o -name '*.db-wal' 2>/dev/null | head -1"
86+
dust: "rm -f *.db-shm *.db-wal (only after confirming no active sqlite process)"
87+
reversible: harmless
88+
89+
# ── Source-Dust ────────────────────────────────────────────────────────
90+
91+
source:
92+
- name: D050-unused-imports
93+
description: "Rust unused imports detected by clippy."
94+
severity: low
95+
probe: "cargo clippy --workspace 2>&1 | grep -q 'unused import'"
96+
dust: "cargo fix --allow-dirty --allow-staged"
97+
reversible: yes
98+
99+
- name: D051-todo-without-issue
100+
description: "TODO/FIXME comments without a tracking-issue reference."
101+
severity: low
102+
probe: "grep -rnE 'TODO|FIXME|XXX' src/ 2>/dev/null | grep -vE '#[0-9]+' | head -1"
103+
dust: "Per-comment: open tracking issue + add // TODO(#N): prefix, or close out the TODO."
104+
reversible: yes
105+
106+
- name: D052-dead-code-warnings
107+
description: "Rust dead_code warnings."
108+
severity: low
109+
probe: "cargo build --workspace 2>&1 | grep -q 'warning: .* is never used'"
110+
dust: "Per-warning: keep if forward-prep (annotate #[allow(dead_code)] with reason), else remove."
111+
112+
# ── Doc-Dust ───────────────────────────────────────────────────────────
113+
114+
docs:
115+
- name: D060-stale-changelog-unreleased
116+
description: "CHANGELOG.md [Unreleased] section has a dated marker > 30 days old."
117+
severity: low
118+
probe: "grep -E '^## \\[Unreleased\\] - ' CHANGELOG.md 2>/dev/null | head -1"
119+
dust: "Either cut a release (move Unreleased → [vX.Y.Z] - <date>) or remove the date from [Unreleased]."
120+
121+
# ── Recovery primitives ────────────────────────────────────────────────
122+
123+
primitives:
124+
- name: source-rollback
125+
description: "Revert all unstaged source changes."
126+
command: "git checkout HEAD -- ."
127+
reversible: yes
128+
129+
- name: branch-cleanup
130+
description: "Prune merged remote-tracking refs."
131+
command: "git fetch --prune"
132+
reversible: yes
133+
134+
dust-philosophy:
135+
- "Idempotent: running dust on a clean repo is a no-op."
136+
- "Narrow blast radius: one action per recipe."
137+
- "Reversibility-first: every dust action undoable from git or by re-running a non-dust recipe."
138+
- "Exclusion list is sacred: active worktrees + production artefacts NEVER swept."

contractiles/must/Mustfile

Lines changed: 98 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,111 @@
11
# SPDX-License-Identifier: PLMP-1.0-or-later
2-
# Mustfile - declarative state contract (template)
3-
# See: https://github.com/hyperpolymath/mustfile
2+
# Mustfile — declarative state contract for echidnabot
3+
# https://github.com/hyperpolymath/echidnabot
4+
#
5+
# Physical-state invariants that MUST hold at all times. Run with:
6+
# just must-check
7+
# Error-code namespace: M###
48

59
version: 1
610

711
metadata:
8-
name: project-state-contract
9-
spec: v0.0.1
10-
description: "Invariant checks for config, policy, gateway, logs, and schema."
12+
name: echidnabot-state-contract
13+
spec: v0.1.0
14+
description: "Invariants for echidnabot: Rust binary, GitHub-App config, DB schema, webhook signatures, dispatch wire."
1115

1216
parameters:
13-
gateway_port: "8080"
14-
schema_version: "v0.0.1"
17+
webhook_port: "8080"
18+
db_url_env: "DATABASE_URL"
19+
echidna_api_env: "ECHIDNA_API_URL"
1520

1621
checks:
17-
- name: config-valid
18-
description: "config/service.yaml must be valid."
19-
run: "yq -e '.' config/service.yaml >/dev/null"
2022

21-
- name: policy-compiles
22-
description: "policy/policy.ncl must compile."
23-
run: "nickel check policy/policy.ncl"
23+
- name: M001-license-present
24+
description: "LICENSE file must exist."
25+
severity: critical
26+
run: "test -f LICENSE || test -f LICENSE.txt"
27+
recovery: "git checkout HEAD -- LICENSE LICENSE.txt"
2428

25-
- name: gateway-exposes-port
26-
description: "Service must expose the configured port."
27-
run: "bash -uc 'ss -lnt | rg \":${GATEWAY_PORT:-8080}\"'"
29+
- name: M002-readme-present
30+
description: "README must exist (adoc canonical; md as renderer-friendly pointer)."
31+
severity: critical
32+
run: "test -f README.adoc"
33+
recovery: "git checkout HEAD -- README.adoc"
2834

29-
- name: logs-are-json
30-
description: "Logs must be JSON."
31-
run: "bash -uc 'rg --files -g \"*.json\" logs | xargs -r jq -e .'"
35+
- name: M003-cargo-toml-present
36+
description: "Cargo.toml at repo root."
37+
severity: critical
38+
run: "test -f Cargo.toml"
39+
recovery: "Manual restore from git history."
3240

33-
- name: schema-version-matches
34-
description: "Schema must match version spec."
35-
run: "bash -uc 'rg -n \"${SCHEMA_VERSION:-v0.0.1}\" schema'"
41+
- name: M004-containerfile-present
42+
description: "Containerfile present (Podman per RSR-H15; Dockerfile forbidden)."
43+
severity: critical
44+
run: "test -f Containerfile"
45+
recovery: "git checkout HEAD -- Containerfile"
46+
47+
- name: M005-no-dockerfile
48+
description: "No Dockerfile (RSR-H15)."
49+
severity: critical
50+
run: "test ! -f Dockerfile"
51+
recovery: "rm -f Dockerfile (should never have been committed)"
52+
53+
- name: M006-no-makefile
54+
description: "No Makefile (RSR-H14: Justfile primary)."
55+
severity: critical
56+
run: "test ! -f Makefile"
57+
recovery: "rm -f Makefile"
58+
59+
- name: M007-no-flake-nix
60+
description: "No flake.nix / flake.lock (nix deprecated estate-wide 2026-06-01)."
61+
severity: critical
62+
run: "test ! -f flake.nix && test ! -f flake.lock"
63+
recovery: "git rm flake.nix flake.lock; commit"
64+
65+
- name: M008-guix-scm-present
66+
description: "guix.scm or .guix-channel exists (guix is the sole reproducible-dev vehicle)."
67+
severity: warning
68+
run: "test -f guix.scm || test -f .guix-channel"
69+
recovery: "Add guix.scm or .guix-channel; owner input on channel URL if missing."
70+
71+
- name: M009-no-secrets-committed
72+
description: "No .env / credentials / private-key files."
73+
severity: critical
74+
run: "test ! -f .env && test ! -f credentials.json && test ! -f .env.local && test ! -f .secrets"
75+
recovery: "git-filter-repo to remove + rotate the secret immediately."
76+
77+
- name: M010-spdx-headers-on-rust
78+
description: "All src/ .rs files carry SPDX-License-Identifier."
79+
severity: warning
80+
run: "find src -name '*.rs' 2>/dev/null | head -50 | xargs -r grep -L 'SPDX-License-Identifier' | wc -l | grep -q '^0$'"
81+
recovery: "Manual per-file addition (per [[feedback_no_automated_licence_edits]] — never sweep)."
82+
83+
- name: M011-cargo-lock-tracked
84+
description: "Cargo.lock committed (echidnabot ships a binary)."
85+
severity: high
86+
run: "test -f Cargo.lock && git ls-files --error-unmatch Cargo.lock > /dev/null"
87+
recovery: "git add Cargo.lock; commit"
88+
89+
- name: M012-gpg-signed-head
90+
description: "HEAD commit is GPG-signed."
91+
severity: critical
92+
run: "git log -1 --pretty='%G?' | grep -qE '[GN]'"
93+
recovery: "git commit --amend -S; git config commit.gpgsign true"
94+
95+
- name: M013-shared-context-dep-shape
96+
description: "Cargo.toml declares the gitbot-shared-context path dep (echidnabot lives in gitbot-fleet/bots/)."
97+
severity: warning
98+
run: "grep -q 'gitbot-shared-context.*path' Cargo.toml"
99+
recovery: "Restore the path dep line (matches gitbot-fleet/shared-context relative location)."
100+
101+
- name: M014-config-example-present
102+
description: "echidnabot.example.toml documents required config keys."
103+
severity: warning
104+
run: "test -f echidnabot.example.toml"
105+
recovery: "Restore from git history; sync with src/config.rs ConfigSchema."
106+
107+
- name: M015-no-tabs-in-cargo-toml
108+
description: "Cargo.toml uses spaces (TOML standard)."
109+
severity: low
110+
run: "! grep -P '\\t' Cargo.toml"
111+
recovery: "sed -i 's/\\t/ /g' Cargo.toml"

0 commit comments

Comments
 (0)