Skip to content
Open
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
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Shell scripts and the compliance-hooks extension must be LF so they run under
# bash/Node on Linux/macOS/WSL even when authored on Windows.
*.sh text eol=lf
*.example text eol=lf
*.mjs text eol=lf
.githooks/pre-push text eol=lf
43 changes: 43 additions & 0 deletions .githooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Git hooks (`.githooks/`)

Repo-tracked git hooks. They are **not** active until you point git at this
directory (hooks can't be auto-installed by `git clone`):

```sh
scripts/dev/install-hooks.sh # sets core.hooksPath=.githooks
# or, equivalently, from the repo root in any shell:
git config core.hooksPath .githooks
```

This sets local config in `.git/config`, so it applies to the whole repo
(including all worktrees on this machine). Run it once per clone.

## Hooks

### `pre-push`
Runs three independent guards on every `git push`. Each **fails open** where it
cannot run, so it never blocks legitimate or offline work.

**1. LEARNINGS.md size-cap guard** (via `scripts/dev/check-learnings-budget.sh`).
`docs/LEARNINGS.md` is loaded into the model's context at the start of every
session, so it is a capped **Tier-1 rules digest** (~2,500 tokens); detailed
narratives belong in `docs/learnings/`. The guard blocks a push when the file
exceeds the cap, forcing priority-based distillation instead of unbounded growth.
- Counts **real tokens** with `tiktoken` when available (`pip install tiktoken`);
otherwise falls back to a dependency-free character proxy.
- Override deliberately with `SKIP_LEARNINGS_BUDGET=1 git push ...`.

**2. Optional project test gate** (via `scripts/dev/pre-push-tests.sh`, if present
and executable). Runs your stack's tests / critical-path eval before allowing the
push. Copy `scripts/dev/pre-push-tests.sh.example` to `pre-push-tests.sh` and fill
in the command. This keeps the *mechanism* (a mechanical test gate on push)
project-agnostic. The example shows both a skippable test run and a NON-bypassable
gate scoped to a critical path.

**3. PR-state guard.**
Blocks pushes to a branch whose PR is already **MERGED** or **CLOSED**. Once a PR
is merged its branch is stale; pushing more commits orphans them (the merged PR
never updates) — the recurring footgun documented in `docs/LEARNINGS.md` §6.
- Uses `gh pr list --head <branch> --state all` to look up PR state.
- Only ever blocks on a confirmed MERGED/CLOSED state with no competing OPEN PR.
- Override deliberately with `SKIP_PR_GUARD=1 git push ...`.
134 changes: 134 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#!/bin/sh
# pre-push -- Mechanical guards that run on every `git push`, regardless of caller
# (agent, IDE, or terminal). Make the rules in docs/LEARNINGS.md mechanical instead
# of relying on memory.
#
# Activate with: scripts/dev/install-hooks.sh (sets core.hooksPath=.githooks)
#
# Three independent guards run in order:
# 1. LEARNINGS.md size-cap guard -- keeps the always-loaded Tier-1 digest small.
# 2. Optional project test gate -- runs scripts/dev/pre-push-tests.sh if present.
# 3. PR-state guard -- blocks pushes to a MERGED/CLOSED PR branch.
#
# Each guard fails OPEN where it cannot run (missing tool, offline) so it never
# blocks legitimate work. Escape hatches are per-guard env vars, noted below.

remote_url="$2"

# Capture git's ref list from stdin into a temp file so it can be read multiple times.
_refs_tmp=$(mktemp)
cat > "$_refs_tmp"
trap 'rm -f "$_refs_tmp"' EXIT

# --- 1. docs/LEARNINGS.md size-cap guard -----------------------------------
# Keeps the always-loaded Tier-1 rules digest under its token cap (see
# docs/LEARNINGS.md "How this file is maintained"). Redirect stdin from /dev/null
# so it never consumes the ref list git feeds us.
# Escape hatch: SKIP_LEARNINGS_BUDGET=1 git push ...
_budget="$(cd "$(dirname "$0")" && pwd)/../scripts/dev/check-learnings-budget.sh"
if [ -f "$_budget" ]; then
sh "$_budget" </dev/null || exit 1
fi
# ---------------------------------------------------------------------------

# --- 2. Optional project test gate -----------------------------------------
# If the project provides scripts/dev/pre-push-tests.sh, run it before allowing
# the push. The script decides WHAT to test (build, unit tests, a critical-path
# eval suite, lint, etc.) and returns non-zero to BLOCK the push. This keeps the
# *mechanism* (a mechanical test gate on push) project-agnostic -- copy
# pre-push-tests.sh.example to pre-push-tests.sh and fill it in.
# Escape hatch (only if the script itself honors it): SKIP_TEST_GUARD=1 git push ...
_tests="$(cd "$(dirname "$0")" && pwd)/../scripts/dev/pre-push-tests.sh"
if [ -f "$_tests" ] && [ -x "$_tests" ]; then
echo "pre-push: running project test gate (scripts/dev/pre-push-tests.sh)..." >&2
REFS_FILE="$_refs_tmp" sh "$_tests" </dev/null || {
echo "" >&2
echo "x pre-push BLOCKED: project test gate failed. Fix failures before pushing." >&2
echo "" >&2
exit 1
}
fi
# ---------------------------------------------------------------------------

# --- 3. PR-state guard -----------------------------------------------------
# Blocks pushes to a branch whose PR is already MERGED or CLOSED. Once a PR is
# merged its branch is stale; pushing more commits orphans them. See LEARNINGS §6.
# Escape hatch (rare, deliberate): SKIP_PR_GUARD=1 git push ...
if [ "${SKIP_PR_GUARD:-}" = "1" ]; then
echo "pre-push: SKIP_PR_GUARD=1 -- skipping PR-state check." >&2
exit 0
fi

if ! command -v gh >/dev/null 2>&1; then
echo "pre-push: 'gh' CLI not found -- skipping PR-state check (fail-open)." >&2
exit 0
fi

# Derive owner/repo from the push URL so the check targets the remote actually
# being pushed to. Only GitHub remotes are checkable.
case "$remote_url" in
https://github.com/*) repo=${remote_url#https://github.com/} ;;
git@github.com:*) repo=${remote_url#git@github.com:} ;;
ssh://git@github.com/*) repo=${remote_url#ssh://git@github.com/} ;;
*) repo="" ;;
esac
repo=${repo%.git}
repo=${repo%/}
if [ -z "$repo" ]; then
echo "pre-push: '$remote_url' is not a recognized GitHub remote -- skipping check." >&2
exit 0
fi

zero="0000000000000000000000000000000000000000"
status=0

# git feeds pre-push one line per ref; we read from the captured temp file.
# <local ref> <local sha> <remote ref> <remote sha>
while read -r local_ref local_sha remote_ref remote_sha; do
# Branch deletion (local sha all-zero) -- nothing to guard.
[ "$local_sha" = "$zero" ] && continue

case "$remote_ref" in
refs/heads/*) branch=${remote_ref#refs/heads/} ;;
*) continue ;; # tags or other refs -- ignore
esac

# The default branch is protected by review/branch rules; never block normal flow.
[ "$branch" = "main" ] && continue
[ "$branch" = "master" ] && continue

# All PRs whose head is this branch. A branch can carry BOTH a merged PR and a
# newer open one, so an OPEN PR must win.
if ! states=$(gh pr list --repo "$repo" --head "$branch" --state all --json state --jq '.[].state' 2>/dev/null); then
echo "pre-push: could not query PR state for '$branch' via gh -- allowing (fail-open)." >&2
continue
fi

# No PRs for this branch -> first push, allow.
[ -z "$states" ] && continue
# An OPEN PR exists -> pushing to it is exactly right.
printf '%s\n' "$states" | grep -q '^OPEN$' && continue
# No open PR, but a merged/closed one exists -> the branch is stale. Block.
closed_state=$(printf '%s\n' "$states" | grep -E '^(MERGED|CLOSED)$' | head -n 1)
[ -z "$closed_state" ] && continue

echo "" >&2
echo "x pre-push BLOCKED: the PR for branch '$branch' is $closed_state ($repo)." >&2
echo " Pushing more commits to it orphans them -- the PR will not update." >&2
echo "" >&2
if [ "$closed_state" = "CLOSED" ]; then
echo " Either reopen that PR first, or move your work to a fresh branch:" >&2
else
echo " Move your work to a fresh, reviewable branch:" >&2
fi
echo " git fetch origin <default-branch>" >&2
echo " git switch -c <new-branch> origin/<default-branch>" >&2
echo " git cherry-pick <your new commit SHAs>" >&2
echo " git push -u origin <new-branch> # then open a new PR" >&2
echo "" >&2
echo " Override (only if you are certain): SKIP_PR_GUARD=1 git push ..." >&2
echo "" >&2
status=1
done < "$_refs_tmp"

exit $status
81 changes: 81 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: "🐛 Bug report (technical)"
description: "Log a defect with technical detail. Critical-path bugs (money / data loss / safety) get a fast lane."
title: "[BUG] <area>: <short summary>"
labels: ["type: bug", "status: triage"]
body:
- type: markdown
attributes:
value: |
Thanks for logging a defect. Keep it concrete. If this bug touches the
**critical path** (money, irreversible actions, data loss, or a core flow being
down), set severity = critical and say so in the summary — those get a fast lane
and a stricter review gate.

- type: textarea
id: summary
attributes:
label: Summary
description: One or two sentences. What is broken?
validations:
required: true

- type: dropdown
id: severity
attributes:
label: Severity (proposed — triage confirms)
options:
- "critical — money/data wrong, safety bypass, data loss, core flow down"
- "high — core flow broken for some users, no clean workaround"
- "medium — feature broken but workaround exists"
- "low — cosmetic / minor / polish"
validations:
required: true

- type: checkboxes
id: layers
attributes:
label: Affected layer(s)
description: "Trace it — most real bugs cross layers."
options:
- label: "Hardware (schematic / PCB / 22V10 GAL / memory map)"
- label: "ROMs / firmware (monitor, BASIC, font / video ROMs)"
- label: "Emulator core + WinUI 3 host (C++ VM)"
- label: "Web (WASM) emulator"
- label: "Disk (WozLib floppy / micro-SD / picodisk)"
- label: "Unknown — needs investigation"

- type: textarea
id: repro
attributes:
label: Repro steps
description: Numbered steps. Include the account/role used.
placeholder: |
1. ...
2. ...
3. Observe ...
validations:
required: true

- type: textarea
id: expected_actual
attributes:
label: Expected vs actual
placeholder: |
Expected: ...
Actual: ...
validations:
required: true

- type: textarea
id: environment
attributes:
label: Environment
description: Build/commit SHA, where observed, browser/device if applicable.
validations:
required: true

- type: textarea
id: evidence
attributes:
label: Evidence (optional)
description: Logs, screenshots, failing test name, relevant queries.
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: "🚨 Urgent: critical-path problem (money / data loss / safety)"
url: https://github.com/ebadger/3ric/issues/new?template=report_a_problem.yml
about: "If the critical path looks wrong, file it AND notify ebadger directly — these are handled out-of-band, not left waiting in a queue."
30 changes: 30 additions & 0 deletions .github/ISSUE_TEMPLATE/report_a_problem.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: "💬 Report a problem (non-technical)"
description: "Something seems wrong but you don't have technical detail. We'll triage it."
title: "[PROBLEM] <short summary>"
labels: ["status: triage"]
body:
- type: markdown
attributes:
value: |
No technical detail needed — just tell us what happened and what you expected.

- type: textarea
id: what_happened
attributes:
label: What happened?
description: Describe it in plain language.
validations:
required: true

- type: textarea
id: expected
attributes:
label: What did you expect to happen?
validations:
required: true

- type: input
id: where
attributes:
label: Where did you see this?
placeholder: "page / screen / time / device"
27 changes: 27 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Summary

<!-- What does this PR do? One or two sentences. -->

## Checklist

<!-- Tick every item that applies. For items that genuinely don't apply, strike through with ~~strikethrough~~ and briefly note why. -->

- [ ] **Spec updated** — if this touches stored data, API shape, or client behaviour, the relevant spec under `specs/` is updated in this commit.
- [ ] **Migration included** — if the data schema changed, a migration is included and tested.
- [ ] **Contract verified** — field names, types, and casing match between the server contract and the client.
- [ ] **Tests pass** — existing tests pass; new behaviour is covered by at least one test.
- [ ] **Only relevant files staged** — no accidental config, build output, or unrelated changes.
- [ ] **Second-model review** — for code/spec/config PRs, both reviewers were run, findings triaged, and the review block added (see `docs/CODE-REVIEW-PANEL.md`). Prose/typo PRs are exempt.

## Second-model review

<!-- For code/spec/config PRs. Delete this section for exempt prose/typo PRs. -->
- Reviewed diff: `<merge-base-sha>...<HEAD-sha>`
- GPT Reviewer (`<model>`): <verdict> — <N> findings
- Gemini Reviewer (`<model>`): <verdict> — <N> findings
- Resolved: <M> fixed, <K> overridden
- Overridden: <finding> — <reason>

## Related issues

<!-- Closes #XXX -->
52 changes: 52 additions & 0 deletions .github/agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Agents (`.github/agents/`)

Custom agents are **specialist lenses** you consult for judgment in their domain —
not a hierarchy you route paperwork through. The main session does the work; agents
sharpen specific decisions. **ebadger is the final authority on everything.**

See `docs/ROLES.md` for the lenses-and-gates org model and `docs/CODE-REVIEW-PANEL.md`
for the review-panel procedure.

## What's here

| File | Purpose |
|------|---------|
| `template-agent.md` | Minimal agent skeleton + the reusable **anti-sycophancy core**. Start here for any new agent. |
| `template-lens-agent.md` | Richer "C-suite lens" skeleton (Identity → Expertise → Decision Authority → Anti-Patterns). Use for a standing specialist advisor. |
| `gpt-reviewer.md` | Independent code reviewer, pinned to a strong **non-Claude** model. Ready to use. |
| `gemini-reviewer.md` | Independent code reviewer, pinned to a strong **third-vendor** model. Ready to use. |

## The two valuable, reusable ideas

### 1. The anti-sycophancy core
The `## Patterns` block (identical on every agent) turns an agreeable assistant into a
colleague who will tell you you're wrong: claim-tagging (`[KNOWN]`/`[INFERRED]`/`[GUESS]`),
explicit confidence bands, "say 'I don't know' on the first line," and a self-audit of
broken rules. This is the single most portable, highest-value part of the template —
keep it on any agent whose job is judgment.

### 2. Model-diverse review as a standing gate
A single model family has consistent blind spots. The two reviewers run on **different
vendors** and are **structurally read-only** (`tools: ["read","search"]` — no edit, no
shell). Before a code PR ships, both review the diff; the primary agent triages every
finding (fix or cite a reason); ebadger merges. The diversity is the entire value — keep
the two reviewers on different vendors, and update the `model:` pins to the current best
models as they change.

## Adding a new agent

1. Copy `template-agent.md` (simple) or `template-lens-agent.md` (rich) to `your-agent.md`.
2. Fill in `name` and a sharp `description` — the description is what gets the agent
invoked at the right time, so name the domain **and** what it must NOT be used for.
3. Keep the `## Patterns` anti-sycophancy block.
4. Narrow `tools:` to what the agent actually needs (read-only advisors get `["read","search"]`).
5. Merge to the default branch to make it available.

## Customize for your project

- Placeholders are already stamped for 3RIC. `template-agent.md` / `template-lens-agent.md` keep their `{{LENS_NAME}}`-style placeholders **on purpose** — they're skeletons to fill in when you add a new agent.
- Update reviewer `model:` pins to the current strongest model from each vendor.
- Build out your own lenses (e.g. Architecture, DevOps, Product, Domain-Expert,
Process & Learning) from `template-lens-agent.md` as the project needs them — but heed
the **mission-clock rule** in `docs/ROLES.md`: don't add org machinery faster than the
product needs it.
Loading