Website for GJH Inc. (gjh-inc.com) — an independent consulting firm helping organizations put AI and their data to practical use.
This repo is a static site, but it is no longer just static. It carries a small self-improving content pipeline (the content loop) that audits, grades, and — when given permission — evolves the site's copy over time. This README documents that architecture and the current operational flow.
- Site layout
- Content loop — overview
- Runtime environments
- Pipeline anatomy
- scoring rubric
- GitHub Actions workflow
- Email notifications
- Human-in-the-loop approval
- Model provider
- Configuration files
- Local development & testing
- Adding a new audited page
- Repo hygiene
| Path | Purpose |
|---|---|
index.html |
Home — the current audit target. Editorial single-pager. |
about.html, services.html |
Legacy pages (older copy, not linked from the new homepage). |
privacy.html, terms.html, 404.html |
Legal / error pages. |
llms.txt |
Machine-readable summary for AI crawlers. |
sitemap.xml |
Site index. |
robots.txt |
Crawl rules. |
assets/ |
Static images / css / js. |
The current homepage is the inspirational focus of the content loop; legacy pages exist but are not audit targets yet.
The loop is an agentic audit-improve cycle. Each cycle:
- Fetches each target page.
- Scores it with both a deterministic static pre-grader and a language model grader against a rubric.
- Trends results in
history/scores.jsonl. - On an OK audit it auto-ships the report/trace to
main. On regression/threshold failure it escalates. It never silently lowers its own standards. - Monthly, it proposes harness improvements (rubric / grader context) as a pull request — always human-gated.
The whole thing runs on GitHub-hosted Ubuntu runners (ubuntu-latest) — zero cost — with the model calls executed through the headless opencode CLI pointed at a free DeepSeek model (no API key).
┌────────────────────────────────────────────┐
fetch │ GitHub Actions │
page ───────────▶│ smoke test ▸ audit ▸ auto-ship ▸ email │
(Loop 1) │ │
inspect │ static pre-grade + model grade (Loop 2) │
(Loop 2a/2b) └────────────────────────────────────────────┘
│ │
history/scores.jsonl reports/<date>-audit.md
│ │
└───────── trends span Loop 4 (improve.py)
| Environment | Where | Used for |
|---|---|---|
| Production | GitHub hosted ubuntu-latest runner |
Scheduled cron (daily + first-of-month); email + auto-ship live. |
| Local | Your machine | loop/audit.py --dry-run etc. — fast, no network, no tokens. |
No self-hosted machine is required. The hosted runners are always online, so the daily schedule always has a place to run, at no cost.
loop/audit.py fetches each targets[].url (config: loop/config.yaml). In dry-run mode it loads a fixture from loop/fixtures/<id>.html instead, exercising grading without the network. Text extraction keeps only real (server-rendered) text — an AI crawler must be able to read the page.
Two independent graders:
- 2a — deterministic static pre-grader (
loop/audit.py): objective checks with no tokens — word count ≥min_rendered_words,<title>, meta description, schema.org JSON-LD, banned-words scan, required positioning signals[consulting, ai, data, assessment, own]. Trends cleanly / deterministically. - 2b — model grader (
loop/audit.py, viacall_model): LLM reads the rubric + intent + pre-grader findings + rendered text and returns a JSON score object. Validation plus auto-retry (up tomax_grader_retries) on malformed / incomplete / out-of-range output. Fails closed if it can never parse. - Scores:
problem_first, falsifiability, differentiation, buyer_fit, proof_density, machine_legibility, voice_discipline, each 0–5, weighted inRUBRIC_WEIGHTS. Weighted overall is the page score.
Every run is appended to the append-only history/scores.jsonl (never overwritten). This gives the system a memory so it can see whether a criterion is a one-off or systemic, and compute movement (last-3 vs first-3 mean).
loop/improve.py reads the trace, finds criteria that keep failing across runs, and — only if there are ≥ MIN_RUNS (3) runs and a criterion recurs ≥ RECURRENCE (3) — writes a proposal to reports/<date>-harness-proposal.md. It never edits the harness directly; it produces a PR for a human to review and merge.
The rubric (loop/rubric.md) defines seven weighted criteria:
| Criterion | Weight | Ideas it guards |
|---|---|---|
problem_first |
1.0 | copy starts from the buyer's problem |
falsifiability |
1.5 | claims are verifiable, never invented metrics/clients |
differentiation |
1.5 | clearly distinct from bulky tech/franchise competitors |
buyer_fit |
1.0 | speaks to commercial / non-profit buyers |
proof_density |
1.0 | tenure, partnerships, named deliverables |
machine_legibility |
1.0 | schema markup, headings, real keys for AI crawlers |
voice_discipline |
0.5 | no buzzwords / hype / "transformative" language |
File: .github/workflows/content-loop.yml
- Triggers:
schedule: cron "0 12 * * *"(daily ≈ 07:00 America/Chicago)schedule: cron "0 14 1 * *"(monthly harness review)workflow_dispatch(manual, with adry_runboolean input)
- Concurrency:
content-loopgroup,cancel-in-progress: false— scheduled runs never overlap. - Permissions:
contents: write,pull-requests: write,issues: write.
audit(daily): checkout → setup python/node → installopencode-ai→ verify it works → smoke test (audit.py --dry-run) → Audit → Auto-ship (ifstatus == ok) → build email body → email daily summary → escalate failures/regressions.improve(monthly or manual only,needs: audit): analyze trends → open harness proposal PR → email notification.notify-failure(needs: [audit, improve],if: failure()): any job dying before its own summary still emails you a failure alert with a run link.
Auto-ship behavior: on a clean audit (status ok), the report + trace are committed directly to main (auto-merged). Anything that could change how future audits are scored (harness via improve) is never auto-merged — it becomes a needs-human-reviewparent PR.
All mail goes to the account set in SMTP_USERNAME (e.g. georjero@gmail.com) via the dawidd6/action-send-mail action against Gmail SMTP.
| When | |
|---|---|
| Daily summary | Only when the audit actually auto-shipped a material change (changed=true). No churn → no email. |
| Harness proposal notification | Only when has_proposal == 'true'. |
| Workflow failure | Any job ended in failure (via notify-failure). Always on, fails soft. |
All mail steps use continue-on-error: true so a transient SMTP outage never fails the run.
- Auto-shipped (no approval needed): daily audit report + trace — these record what is happening, they don't alter the site's copy or the scoring rules.
- Needs your approval (via PR merge): every harness change (rubric, grader context, config). The loop writes a proposal
md; the workflow opens a PR labeledneeds-human-review; merging that PR is the approval.
No API key is required. Model work runs through the headless opencode CLI:
opencode run -m opencode/deepseek-v4-flash-free --format json --pure --auto "<system>...\n\n<prompt>"
call_model()(inloop/audit.py) shells out toopencode, parses the JSON event stream, and returns the model'stext.- Model IDs live in
loop/config.yamlundermodels:(grader,improver).
This removes the old Anthropic HTTP dependency entirely.
| File | Purpose |
|---|---|
loop/config.yaml |
Targets, competitors, models, thresholds, static checks, content loop parameters. |
loop/rubric.md |
The seven human-written scoring criteria the model grader reads. |
loop/audit.py |
Loop 1+2 — fetch, static check, LLM grade, weighted score, report, trace, call_model → opencode. |
loop/improve.py |
Loop 4 — trend analysis + harness proposal generation. |
loop/fixtures/<id>.html |
Dry-run fixtures for each audited target. |
.github/workflows/content-loop.yml |
The CI/CD job wiring (see above). |
loop/requirements.txt |
Python deps (requests, pyyaml). |
# dry-run against fixtures (no network, no model, deterministic)
python loop/audit.py --dry-run
# live against the config's URLs (needs only the opencode CLI ^. It will call the free
# DeepSeek model headlessly; no API key.)
opencode run -m opencode/deepseek-v4-flash-free --format json "smoke"
python loop/audit.py
# trend analysis / harness proposal (dry)
python loop/improve.py --dry-run
python loop/improve.py --dry-run --include-dry-runs # include dry-run traces in the trendTo install opencode locally:
npm i -g opencode-ai@latestTo serve the site locally:
python3 -m http.server 5000 --bind 0.0.0.0
# then http://localhost:5000/- Add a new target in
loop/config.yamlundertargets:with a uniqueid,url, andintent. - Optionally add static checks / signals relevant to that page.
- Add/re-cross-sync a fixture at
loop/fixtures/<id>.htmlso dry-run works offline. - Add the page to
llms.txtandsitemap.xmlif it should be crawled. - Push. The loop will fetch + grade it, and add the page to the report + trace automatically.
.
├── .github/workflows/content-loop.yml # GH Actions orchestration
├── loop/ # the content-loop harness
│ ├── audit.py, improve.py, rubric.md, config.yaml, requirements.txt, fixtures/
├── history/ # scores trace (jsonl, append-only)
├── reports/ # generated audit / proposal markdown
├── index.html, about.html, services.html, ... # pages
├── llms.txt, sitemap.xml, robots.txt
└── attached_assets/ # source/design reference docs
history/,reports/, andloop/config-live.yaml(local test config) are gitignored but nowreports/historyare committed by the loop's auto-ship so trends persist across runs.