Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Reusable autofix — the "fix it for me" button. Add a label to a PR and CI runs
# `mise run fix`, commits the result to the PR branch, and drops the label.
#
# # .github/workflows/autofix.yml (caller)
# # .github/workflows/_autofix.yml (caller)
# name: autofix
# on:
# pull_request:
# types: [labeled]
# jobs:
# autofix:
# uses: CMaintz/foundry/.github/workflows/autofix.yml@v1
# uses: CMaintz/foundry/.github/workflows/_autofix.yml@v1
# permissions:
# contents: write
# pull-requests: write
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# jobs:
# bootstrap:
# uses: CMaintz/foundry/.github/workflows/bootstrap.yml@<sha>
# uses: CMaintz/foundry/.github/workflows/_bootstrap.yml@<sha>
# with:
# habit_hooks_plugin: habit-hooks-typescript
# permissions:
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/tier0.yml → .github/workflows/_guards.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Reusable Tier 0 — language-agnostic checks that run on every repo.
# Reusable "guards" — language-agnostic checks that run on every repo: secret scan +
# ruleset-guard. (Formerly tier0.yml.)
#
# jobs:
# tier0:
# uses: CMaintz/foundry/.github/workflows/tier0.yml@v1
# guards:
# uses: CMaintz/foundry/.github/workflows/_guards.yml@v1
#
# Nothing here knows what language the repo is written in.
name: tier0
name: guards

on:
workflow_call:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/java.yml → .github/workflows/_java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# jobs:
# gate:
# uses: CMaintz/foundry/.github/workflows/java.yml@<sha>
# uses: CMaintz/foundry/.github/workflows/_java.yml@<sha>
# with:
# spotbugs: true # opt-in bytecode analysis
#
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/php.yml → .github/workflows/_php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# jobs:
# gate:
# uses: CMaintz/foundry/.github/workflows/php.yml@<sha>
# uses: CMaintz/foundry/.github/workflows/_php.yml@<sha>
#
# Runs the six verbs a developer runs locally (`mise run gate`). PHP toolchain
# comes from the repo's mise.toml; Composer is preinstalled on the runner.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
# jobs:
# ratchet:
# uses: CMaintz/foundry/.github/workflows/ratchet-report.yml@<sha>
# uses: CMaintz/foundry/.github/workflows/_ratchet-report.yml@<sha>
# with:
# baselines: |
# eslint-suppressions.json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# jobs:
# sast:
# uses: CMaintz/foundry/.github/workflows/semgrep.yml@<sha>
# uses: CMaintz/foundry/.github/workflows/_semgrep.yml@<sha>
#
# CodeQL is the GitHub-native alternative but is free only on PUBLIC repos;
# Semgrep is free and works on private repos too, so it's the portable default.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ts.yml → .github/workflows/_ts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# jobs:
# gate:
# uses: CMaintz/foundry/.github/workflows/ts.yml@v1
# uses: CMaintz/foundry/.github/workflows/_ts.yml@v1
#
# Runs the same six verbs a developer runs locally. If this and `mise run gate`
# on a laptop ever disagree, that is a bug in the setup, not in the code.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/web.yml → .github/workflows/_web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# jobs:
# web:
# uses: CMaintz/foundry/.github/workflows/web.yml@<sha>
# uses: CMaintz/foundry/.github/workflows/_web.yml@<sha>
# with: { max_lines: 400 }
#
# File-length limits apply to markup and styles too — a 2000-line stylesheet is a
Expand Down
4 changes: 2 additions & 2 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ cadences, which is what justifies the split.
C:\Users\akash\Projects\_foundry\ ← leading underscore: sorts above the IDE dirs,
│ reads as "not an application"
├── foundry/ → github.com/CMaintz/foundry
│ ├── .github/workflows/ reusable workflow_call: tier0.yml, ts.yml, jvm.yml, php.yml
│ ├── .github/workflows/ reusable workflow_call: _guards.yml, _ts.yml, _java.yml, _php.yml
│ ├── mise/ shared task templates per stack
│ ├── presets/ eslint / tsconfig / ruff / phpstan base configs
│ ├── habit-hooks/ config.toml presets per stack
Expand All @@ -323,7 +323,7 @@ C:\Users\akash\Projects\_foundry\ ← leading underscore: sorts above the
└── CONTRACT.md ◀── same file, kept in sync
```

- `foundry` is consumed **by repos**, via `uses: CMaintz/foundry/.github/workflows/ts.yml@v1`.
- `foundry` is consumed **by repos**, via `uses: CMaintz/foundry/.github/workflows/_ts.yml@v1`.
- `cmaintz-skills` is consumed **by the agent**, via `/plugin marketplace add CMaintz/cmaintz-skills`.

**The seam is `CONTRACT.md`** — the verb interface of §3, copied verbatim into both. As long as both
Expand Down
16 changes: 8 additions & 8 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ mechanism* (not just an example) → a per-language file (`mise/<lang>.toml`,

| Workflow | Purpose |
|---|---|
| `ts.yml` · `java.yml` · `php.yml` | Language gate — the six verbs, decomposed one-per-step with targeted failure summaries, + a structural-smells job (which prints a per-smell "what it means / fix toward" legend on failure). `java` adds opt-in `spotbugs` / `no_var`. node_modules / vendor / Gradle caching. |
| `tier0.yml` | Language-agnostic: secret scan (gitleaks) + `ruleset-guard`, both with remediation step-summaries. Merge-base–scoped. |
| `semgrep.yml` | SAST, diff-aware (`--baseline-commit`), pip-cached, pinnable. |
| `web.yml` | Max-file-length gate for HTML/CSS. |
| `bootstrap.yml` | Regenerate the habit-hooks snooze baseline on Linux (`--prune` to shrink), open a PR. |
| `ratchet-report.yml` | PR comment showing how the accepted-debt baselines moved. |
| `autofix.yml` | Label a PR `autofix` → runs `mise run fix`, commits + pushes the result. |
| `lint-workflows.yml` | actionlint over foundry's own workflows. |
| `_ts.yml` · `_java.yml` · `_php.yml` | Language gate — the six verbs, decomposed one-per-step with targeted failure summaries, + a structural-smells job (which prints a per-smell "what it means / fix toward" legend on failure). `_java.yml` adds an opt-in `spotbugs` job (no-`var` is folded into `lint`). node_modules / vendor / Gradle caching. |
| `_guards.yml` | Language-agnostic: secret scan (gitleaks) + `ruleset-guard`, both with remediation step-summaries. Merge-base–scoped. |
| `_semgrep.yml` | SAST, diff-aware (`--baseline-commit`), pip-cached, pinnable. |
| `_web.yml` | Max-file-length gate for HTML/CSS. |
| `_bootstrap.yml` | Regenerate the habit-hooks snooze baseline on Linux (`--prune` to shrink), open a PR. |
| `_ratchet-report.yml` | PR comment showing how the accepted-debt baselines moved. |
| `_autofix.yml` | Label a PR `autofix` → runs `mise run fix`, commits + pushes the result. |
| `lint-workflows.yml` | actionlint + shellcheck + typos over foundry's own repo (not reusable — no `_`). |

## mise verb templates (`mise/`)

Expand Down
20 changes: 10 additions & 10 deletions OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Three probabilistic *roles* are kept deliberately separate (people mush them):

## 5. The gate, job by job

The CI gate (`gate.yml`, or foundry's reusable `ts.yml` + `tier0.yml`) is four
The CI gate (`gate.yml`, or foundry's reusable `_ts.yml` + `_guards.yml`) is four
jobs. CI is **100% deterministic on purpose** — no model, no API key, no secrets
beyond the default token.

Expand Down Expand Up @@ -164,7 +164,7 @@ lets `mise run fix` prune the baseline and ship that in the same PR as the fix.
### What counts as a gate-defining file — and who watches it

Governance is split across three enforcement points by *when* they run — a
client-side hook (cmaintz-skills), the CI guard (`tier0.yml`), and the regenerator
client-side hook (cmaintz-skills), the CI guard (`_guards.yml`), and the regenerator
(`bootstrap.yml`). This table is the single map of what's protected and how:

| File | Controls | Protected by |
Expand All @@ -173,7 +173,7 @@ client-side hook (cmaintz-skills), the CI guard (`tier0.yml`), and the regenerat
| `eslint-suppressions.json` | lint baseline | same three |
| `.jscpd.json` | which paths the duplication sensor scans | **ruleset-guard** — widening the ignore list alongside source needs the label |
| `pmd/ruleset.xml`, thresholds, `mise.toml`, workflows | rule definitions / gate wiring | **ruleset-guard** — direction can't be proven, so any change alongside source needs the label |
| `ruleset_paths` regex | *which* files ruleset-guard treats as gate-defining (this table's first column) | the `tier0` input default, or a consumer's override — **keep it in sync when you add a guarded file** |
| `ruleset_paths` regex | *which* files ruleset-guard treats as gate-defining (this table's first column) | the `guards` input default, or a consumer's override — **keep it in sync when you add a guarded file** |

The recurring failure mode: a *new* gate-defining file (like `.jscpd.json`) isn't
added to `ruleset_paths`, so widening it slips past the guard. When you introduce
Expand Down Expand Up @@ -250,7 +250,7 @@ work → habit-hooks flags a smell → agent fixes it → /learn decides:

## 10. Language coverage — where it actually is

Proven and built out: **TypeScript / Node** (`mise/ts.toml`, `ts.yml`, eslint +
Proven and built out: **TypeScript / Node** (`mise/ts.toml`, `_ts.yml`, eslint +
tsc + vitest + habit-hooks-typescript).

Reachable cheaply (habit-hooks already has detectors): **Java** (PMD), **PHP**
Expand Down Expand Up @@ -318,21 +318,21 @@ radius* into separate workflow files, each composing foundry's reusable pieces:

| File | Contains | Trigger |
|---|---|---|
| `gate.yml` | the deterministic gate(s) — `ts.yml` / language gate per package | PR + push |
| `gate.yml` | the deterministic gate(s) — `_ts.yml` / language gate per package | PR + push |
| `quality.yml` | structural smells (habit-hooks), ratchet checks | PR + push |
| `security.yml` | `tier0.yml` (secrets + ruleset-guard) + `semgrep.yml` | PR + push |
| `bootstrap.yml` | `bootstrap.yml` — baseline refresh | `workflow_dispatch` |
| `security.yml` | `_guards.yml` (secrets + ruleset-guard) + `_semgrep.yml` | PR + push |
| `bootstrap.yml` | `_bootstrap.yml` — baseline refresh | `workflow_dispatch` |
| `deploy.yml` | image publish / release — **push-to-main only** | push |

```yaml
# .github/workflows/security.yml
name: security
on: { pull_request: {}, push: { branches: [main] } }
jobs:
tier0:
uses: CMaintz/foundry/.github/workflows/tier0.yml@<sha>
guards:
uses: CMaintz/foundry/.github/workflows/_guards.yml@<sha>
sast:
uses: CMaintz/foundry/.github/workflows/semgrep.yml@<sha>
uses: CMaintz/foundry/.github/workflows/_semgrep.yml@<sha>
```

Why split, not one file: `needs:` can't cross workflow files, so unrelated jobs
Expand Down
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,24 @@ Or wire it by hand — a `mise.toml` with the six verbs (see [templates/](./mise
# .github/workflows/gate.yml
jobs:
gate:
uses: CMaintz/foundry/.github/workflows/java.yml@v1
uses: CMaintz/foundry/.github/workflows/_java.yml@v1
```

### Reusable workflows

Reusable workflows are `_`-prefixed to group them in the file listing — GitHub requires
every workflow file at the top level of `.github/workflows/`, so a filename prefix is the
only grouping it allows. Consumers reference them by path (`uses: …/_java.yml@v1`).

| Workflow | What it runs |
|---|---|
| `ts.yml` · `java.yml` · `php.yml` | the language gate (six verbs, decomposed one-per-step with targeted fix summaries) + structural smells (which print a per-smell "fix toward" legend on failure). `java` adds opt-in `spotbugs` / `no_var` jobs |
| `tier0.yml` | language-agnostic: secret scan + `ruleset-guard` |
| `semgrep.yml` | SAST, diff-aware (only new findings fail) |
| `web.yml` | max-file-length gate for HTML/CSS |
| `bootstrap.yml` | regenerate the habit-hooks snooze baseline on Linux, open a PR |
| `ratchet-report.yml` | PR comment showing how the accepted-debt baselines moved |
| `autofix.yml` | add an `autofix` label to a PR → runs `mise run fix`, commits + pushes the result |
| `_ts.yml` · `_java.yml` · `_php.yml` | the language gate (six verbs, decomposed one-per-step with targeted fix summaries) + structural smells (which print a per-smell "fix toward" legend on failure). `_java.yml` adds an opt-in `spotbugs` job (the no-`var` rule is folded into `lint`) |
| `_guards.yml` | language-agnostic: secret scan + `ruleset-guard` |
| `_semgrep.yml` | SAST, diff-aware (only new findings fail) |
| `_web.yml` | max-file-length gate for HTML/CSS |
| `_bootstrap.yml` | regenerate the habit-hooks snooze baseline on Linux, open a PR |
| `_ratchet-report.yml` | PR comment showing how the accepted-debt baselines moved |
| `_autofix.yml` | add an `autofix` label to a PR → runs `mise run fix`, commits + pushes the result |

Split them across `gate.yml` / `quality.yml` / `security.yml` / `bootstrap.yml` (see [OVERVIEW.md](./OVERVIEW.md) §13).

Expand Down
18 changes: 9 additions & 9 deletions scripts/foundry-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ RAW="https://raw.githubusercontent.com/$REPO/$REF"
# HH = the habit-hooks preset basename (presets/habit-hooks/<HH>.toml). It tracks the
# plugin/language name, so `ts` maps to `typescript` — the others match the stack.
case "$STACK" in
ts) PLUGIN="habit-hooks-typescript"; CI="ts.yml"; HH="typescript" ;;
java) PLUGIN="habit-hooks-java"; CI="java.yml"; HH="java" ;;
php) PLUGIN="habit-hooks-php"; CI="php.yml"; HH="php" ;;
ts) PLUGIN="habit-hooks-typescript"; CI="_ts.yml"; HH="typescript" ;;
java) PLUGIN="habit-hooks-java"; CI="_java.yml"; HH="java" ;;
php) PLUGIN="habit-hooks-php"; CI="_php.yml"; HH="php" ;;
kotlin|dotnet|python) PLUGIN=""; CI=""; HH="$STACK" ;; # mise template only; inline gate
*) echo "unknown stack: $STACK" >&2; exit 2 ;;
esac
Expand Down Expand Up @@ -134,18 +134,18 @@ name: security
on: { pull_request: {}, push: { branches: [main] } }
concurrency: { group: security-\${{ github.ref }}, cancel-in-progress: true }
jobs:
tier0:
uses: $REPO/.github/workflows/tier0.yml@$REF
guards:
uses: $REPO/.github/workflows/_guards.yml@$REF
sast:
uses: $REPO/.github/workflows/semgrep.yml@$REF
uses: $REPO/.github/workflows/_semgrep.yml@$REF
YAML

write ".github/workflows/ratchet.yml" <<YAML
name: ratchet
on: { pull_request: {} }
jobs:
ratchet:
uses: $REPO/.github/workflows/ratchet-report.yml@$REF
uses: $REPO/.github/workflows/_ratchet-report.yml@$REF
permissions: { contents: read, pull-requests: write }
YAML

Expand All @@ -158,7 +158,7 @@ on:
- cron: '0 6 * * *' # daily auto-prune — baseline shrinks on its own, one PR/day max
jobs:
bootstrap:
uses: $REPO/.github/workflows/bootstrap.yml@$REF
uses: $REPO/.github/workflows/_bootstrap.yml@$REF
with:
working_directory: "$WD"
habit_hooks_plugin: "$PLUGIN"
Expand All @@ -180,7 +180,7 @@ cat <<'NEXT'
3. Turn on Renovate (or Dependabot) so the pins you just set stay fresh.
4. Branch protection on `main` (Settings -> Branches), require these checks:
- Deterministic gate (gate.yml)
- Secret scan, Ruleset guard (security.yml / tier0)
- Secret scan, Ruleset guard (security.yml / _guards)
- SAST (security.yml / semgrep)
- Structural smells (once a baseline exists)
Gotchas: required-check names must match the job name EXACTLY; do NOT require
Expand Down
Loading