Skip to content
Merged
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
33 changes: 33 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## Summary

<!-- 1-3 bullets. What and why. -->

-

## SDD checklist

<!-- Enforced by .github/workflows/spec-gate.yml. See AGENTS.md "Discipline Gate". -->

- [ ] Vault backlog entry exists in `~/Projects/knowledge/10_projects/dotfiles/11-tasks.md`
- [ ] Spec folder `specs/<feature-id>/` is included in this PR (or `skip-sdd` label below)
- [ ] `proposal.md` has filled Why / What / Acceptance criteria
- [ ] `tasks.md` is in TDD order
- [ ] `verification.md` will be filled before merge (evidence + commit hashes)

## SDD skip rationale

<!--
Only fill this section if you are adding the "skip-sdd" label to this PR.
Provide a real, specific reason. Examples:
- "Mechanical rename across 12 files; no logic change."
- "Pure formatting pass after rustfmt config update."
- "Documentation-only refactor of 6 README files into pointer-style."

Empty rationale = the spec-gate fails even with the "skip-sdd" label.
-->

## Test plan

- [ ] `~/.local/bin/bats tests/*.bats` passes
- [ ] `~/.local/bin/shellcheck` clean on changed `.sh`
- [ ] Manual verification (describe):
33 changes: 33 additions & 0 deletions .github/workflows/spec-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: spec-gate

# SDD Tier 4 enforcement: every PR >=50 LOC of production diff must include
# an active specs/<feature-id>/ folder. See AGENTS.md "Discipline Gate".

on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled, edited]
branches: [main]

jobs:
spec-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Fetch base ref
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
run: git fetch --no-tags --prune --depth=1 origin "$BASE_REF"

- name: Run SDD spec-gate
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
SDD_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
SDD_PR_BODY: ${{ github.event.pull_request.body }}
run: |
./scripts/check-spec-gate.sh \
--base-ref "origin/$BASE_REF" \
--head-ref HEAD \
--explain
9 changes: 9 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,12 @@ repos:
language: script
pass_filenames: false
stages: [pre-commit]

- id: sdd-spec-gate
name: SDD spec-gate (Tier 4)
entry: ./scripts/check-spec-gate.sh
args: ['--base-ref', 'origin/main', '--head-ref', 'HEAD']
language: script
pass_filenames: false
stages: [pre-push]
always_run: true
32 changes: 32 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,38 @@ Senior Principal Software Architect & Technical Mentor. 20+ years production exp
| Workflow | workflow-protocol, decision-persistence, fix-small-debt |
| Domain | matlab-embedded, matlab-scientific, corporate-network-constraints |

## Model Selection (Task-Aware)

Match model power to task complexity. Goal: maximum capability where it matters, minimum token cost where it doesn't. Provider-agnostic principle; concrete model names live in per-agent overlay files.

### Tier Mapping

| Tier | Use for | Why |
|---|---|---|
| **Top** | Hard debugging, root-cause analysis, distributed systems, concurrency, security review, schema design, novel architecture, complex refactors with semantic risk | Reasoning depth dominates; a wrong answer is expensive to undo |
| **Mid** | Mechanical refactors, single-file fixes, documentation, boilerplate generation, regex / JSON parsing, test scaffolding, comment-only edits | Capability is sufficient; token savings real |
| **Low** | Syntax lookups, quick questions, autocomplete, one-line transforms, "what's the flag for X" | Latency + cost dominate; capability is overkill |

### Trigger Heuristics

Agents SHOULD **propose** a tier change when they detect a task-class shift mid-session. The user decides. Examples:

- "Architectural design is done; remaining work is 6 file edits applying the schema. Want to switch to Mid for the implementation phase?"
- "This was supposed to be a refactor but we hit a concurrency bug. Want to switch to Top for the debug?"

Do NOT auto-switch silently. Auto-switching breaks the user's expectations about cost and capability — the proposal IS the value.

### Per-Provider Overlays

Concrete model identifiers per tier live in the agent-specific overlay files:

- `ai/claude/CLAUDE.md` — Claude Code (subagent frontmatter `model: opus|sonnet|haiku`; main session `/model` slash)
- `ai/opencode/opencode.jsonc` — OpenCode (TUI `/models` picker; `qq` / `qf` wrappers for quick-questions)
- `ai/gemini/GEMINI.md` — Gemini CLI (per-prompt `--model` flag)
- `ai/copilot/copilot-instructions.md` — GitHub Copilot CLI v2 (TBD; concrete schema pending AI-017/AI-018 audit)

Model names rotate; tier semantics are stable. When a provider releases a new flagship or sunsets a tier, edit ONLY the relevant overlay — `AGENTS.md` does not need a corresponding patch.

## Competence Retention Protocol (Anti-Atrophy)

Strict distinction of tasks to prevent skill erosion. Do not be a crutch.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ Full reference and pane-layout recipes: `~/Projects/knowledge/10_projects/dotfil

**Recommended:** age, gh (GitHub CLI), direnv, zoxide, eza

## Contributing

PRs ≥50 LOC of production diff must include an active `specs/<feature-id>/` folder (Spec-Driven Development). The `spec-gate` CI check enforces this; failures link back to `AGENTS.md` "Discipline Gate". Escape hatch: add the `skip-sdd` label AND a non-empty `## SDD skip rationale` section in the PR body. Optional local pre-push hook: `./scripts/install-precommit.sh --with-sdd-gate`.

## Documentation

Detailed documentation lives in the private knowledge vault:
Expand Down
8 changes: 8 additions & 0 deletions ai/claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,11 @@ Claude reads memory in this order at session start:
4. **Auto memory:** `~/.claude/projects/<project-hash>/memory/MEMORY.md` (cross-session continuity).

If both `CLAUDE.md` and `AGENTS.md` exist in a repo, `AGENTS.md` is authoritative for behavioural rules; `CLAUDE.md` overlays Claude-specific tooling notes on top.

## Model Tier (per AGENTS.md "Model Selection")

- **Top:** `claude-opus-4-7` — hard debug / architecture / root-cause / Socratic Guardrail triggers
- **Mid:** `claude-sonnet-4-6` — mechanical refactor / docs / single-file fixes / test scaffolding
- **Low:** `claude-haiku-4-5-20251001` — syntax lookups / quick questions

Subagent declaration: `model: opus|sonnet|haiku` in frontmatter. Main session: `/model` slash command.
6 changes: 6 additions & 0 deletions ai/copilot/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@
* **FAE tickets:** `50_work/tickets/`.

Full vault hierarchy and frontmatter law live in `AGENTS.md` § Vault Structure & Standards.

## Model Tier (per AGENTS.md "Model Selection")

- **Top / Mid / Low:** TBD — concrete model identifiers pending AI-017 (skills port) and AI-018 (MCP deploy) audits on a Windows admin machine where `copilot` v2 is installed. Until then, follow AGENTS.md tier semantics and use whatever default the v2 CLI provides.

When AI-017/AI-018 close, replace this block with the literal model IDs.
8 changes: 8 additions & 0 deletions ai/gemini/GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ In addition to the Response Protocol in `AGENTS.md`:

* Generate **full files or precise diffs** — Gemini's context window makes full-file outputs cheaper for the user to review than diffs in many cases. Choose based on file size and change density.
* **No Fluff:** No intro/outro conversational filler. Markdown headings and code fences only when they aid scanning.

## Model Tier (per AGENTS.md "Model Selection")

- **Top:** `gemini-2.5-pro` — hard debug / architecture / root-cause (TBD — empirically verify on next Gemini session)
- **Mid:** `gemini-2.5-flash` — mechanical refactor / docs / single-file fixes
- **Low:** `gemini-2.5-flash-lite` — syntax lookups / autocomplete

Selection: per-prompt `--model` flag on the Gemini CLI. Model IDs marked TBD pending empirical verification.
6 changes: 6 additions & 0 deletions ai/opencode/opencode.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
// Deploy target: ~/.config/opencode/opencode.jsonc (via setup-linux.sh).
// Schema: https://opencode.ai/config.json
// Spec: specs/archive/AI-011-opencode-bootstrap/ (archived 2026-05-17)
//
// Model Tier (per AGENTS.md "Model Selection"):
// Top: opencode-go/deepseek-v4-pro — hard debug / architecture / root-cause
// Mid: opencode-go/qwen3.6-plus — refactor / docs / multilingual (ES) quick-questions via `qq`
// Low: opencode-go/deepseek-v4-flash — autocomplete / one-line transforms via `qf` (~97 tok/s)
// Selection: TUI `/models` picker. Wrappers `qq` / `qf` defined in .zsh/aliases.zsh + .bashrc + powershell/profile.ps1.
{
"$schema": "https://opencode.ai/config.json",

Expand Down
198 changes: 198 additions & 0 deletions scripts/check-spec-gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/bin/bash

# check-spec-gate.sh: SDD Tier 4 enforcement gate.
#
# Computes production diff LOC between two refs and validates that PRs above
# the threshold include at least one file under specs/<feature-id>/ (active
# spec folder, NOT specs/archive/). Used by .github/workflows/spec-gate.yml
# and opt-in by pre-push hooks installed via scripts/install-precommit.sh.
#
# Usage:
# check-spec-gate.sh --base-ref REF --head-ref REF [--threshold N] [--explain]
#
# Env (consumed when set, normally populated by the CI workflow from PR):
# SDD_LABELS Comma-separated PR labels
# SDD_PR_BODY PR body text
#
# Exit:
# 0 OK (under threshold OR spec folder present OR valid skip)
# 1 Discipline Gate violation
# 2 Usage/setup error

set -euo pipefail

THRESHOLD=50
BASE_REF=""
HEAD_REF=""
EXPLAIN=0

usage() {
cat <<'EOF'
Usage: check-spec-gate.sh --base-ref REF --head-ref REF [--threshold N] [--explain]

--base-ref REF Base ref to diff against (e.g. origin/main)
--head-ref REF Head ref of the change (e.g. HEAD)
--threshold N LOC threshold above which a spec folder is required (default 50)
--explain Print the LOC breakdown per file
-h, --help Show this help

Env:
SDD_LABELS Comma-separated PR labels (CI sets this; locally optional)
SDD_PR_BODY PR body text (CI sets this; locally optional)

Exit codes:
0 OK
1 Discipline Gate violation
2 Usage/setup error
EOF
}

while [[ $# -gt 0 ]]; do
case "$1" in
--base-ref) BASE_REF="$2"; shift 2 ;;
--head-ref) HEAD_REF="$2"; shift 2 ;;
--threshold) THRESHOLD="$2"; shift 2 ;;
--explain) EXPLAIN=1; shift ;;
-h|--help) usage; exit 0 ;;
*) printf '[ERROR] Unknown argument: %s\n' "$1" >&2; usage >&2; exit 2 ;;
esac
done

if [[ -z "$BASE_REF" || -z "$HEAD_REF" ]]; then
printf '[ERROR] --base-ref and --head-ref are required\n' >&2
usage >&2
exit 2
fi

if ! git rev-parse --git-dir >/dev/null 2>&1; then
printf '[ERROR] Not in a git repo\n' >&2
exit 2
fi

SDD_LABELS="${SDD_LABELS:-}"
SDD_PR_BODY="${SDD_PR_BODY:-}"

_has_label() {
case ",${SDD_LABELS}," in
*",${1},"*) return 0 ;;
*) return 1 ;;
esac
}

_skip_rationale_nonempty() {
local extracted
extracted=$(printf '%s\n' "$SDD_PR_BODY" | awk '
/^## SDD skip rationale[[:space:]]*$/ { in_block=1; next }
in_block && /^## / { exit }
in_block { print }
' | tr -d '[:space:]')
[[ -n "$extracted" ]]
}

_excluded() {
local path="$1"
local base="${path##*/}"
case "$path" in
tests/*|specs/archive/*) return 0 ;;
*generated*) return 0 ;;
esac
case "$base" in
*.lock|*.lockb) return 0 ;;
package-lock.json|pnpm-lock.yaml|go.sum) return 0 ;;
.gitignore|CHANGELOG.md) return 0 ;;
esac
return 1
}

_is_active_spec_path() {
case "$1" in
specs/archive/*) return 1 ;;
specs/*/*) return 0 ;;
*) return 1 ;;
esac
}

if _has_label "dependencies"; then
printf '[OK] spec-gate skipped: PR carries "dependencies" label (dependabot/renovate)\n'
exit 0
fi

if _has_label "skip-sdd"; then
if _skip_rationale_nonempty; then
printf '[OK] spec-gate skipped: "skip-sdd" label + non-empty "## SDD skip rationale" in PR body\n'
exit 0
fi
cat >&2 <<'EOF'
[FAIL] "skip-sdd" label present but the "## SDD skip rationale" section is empty or missing in the PR body.
Add a "## SDD skip rationale" section to the PR body with a real reason
(e.g. "mechanical rename, no logic change"), or open a spec folder instead.
EOF
exit 1
fi

TOTAL_LOC=0
SPEC_TOUCHED=0
INCLUDED=()
EXCLUDED=()

while IFS=$'\t' read -r added removed path; do
[[ -z "${path:-}" ]] && continue
[[ "$added" == "-" ]] && added=0
[[ "$removed" == "-" ]] && removed=0

if _is_active_spec_path "$path"; then
SPEC_TOUCHED=1
fi

if _excluded "$path"; then
EXCLUDED+=("$path:$((added + removed))")
continue
fi

file_loc=$((added + removed))
TOTAL_LOC=$((TOTAL_LOC + file_loc))
INCLUDED+=("$path:$file_loc")
done < <(git diff --numstat "${BASE_REF}...${HEAD_REF}" 2>/dev/null || true)

if [[ "$EXPLAIN" -eq 1 ]]; then
printf '[INFO] Spec-gate breakdown (%s...%s)\n' "$BASE_REF" "$HEAD_REF"
printf ' Threshold: %d LOC\n' "$THRESHOLD"
printf ' Production LOC (added+removed): %d\n' "$TOTAL_LOC"
if [[ "$SPEC_TOUCHED" -eq 1 ]]; then
printf ' Spec folder touched: yes\n'
else
printf ' Spec folder touched: no\n'
fi
if (( ${#INCLUDED[@]} > 0 )); then
printf ' Files counted:\n'
for f in "${INCLUDED[@]}"; do printf ' %s\n' "$f"; done
fi
if (( ${#EXCLUDED[@]} > 0 )); then
printf ' Files excluded:\n'
for f in "${EXCLUDED[@]}"; do printf ' %s\n' "$f"; done
fi
fi

if (( TOTAL_LOC < THRESHOLD )); then
printf '[OK] Production diff %d LOC < threshold %d (below threshold; spec not required)\n' "$TOTAL_LOC" "$THRESHOLD"
exit 0
fi

if [[ "$SPEC_TOUCHED" -eq 1 ]]; then
printf '[OK] Production diff %d LOC >= threshold %d AND spec folder touched in diff\n' "$TOTAL_LOC" "$THRESHOLD"
exit 0
fi

cat >&2 <<EOF
[FAIL] SDD Discipline Gate violation:
Production diff: $TOTAL_LOC LOC (>= threshold $THRESHOLD)
No specs/<feature-id>/ folder touched in this PR.

Options:
(a) Create a spec folder: ./scripts/init-spec.sh <feature-id>
(b) Add the "skip-sdd" label to the PR AND a non-empty
"## SDD skip rationale" section in the PR body.

Reference: AGENTS.md "Discipline Gate (NON-NEGOTIABLE)" section.
EOF
exit 1
Loading
Loading