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
4 changes: 2 additions & 2 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "meta-skills",
"version": "4.3.0",
"description": "Enterprise Quality Engine — 16 skills, 17 commands, 12 hooks across 7 events. Opus 4.7 + T-scale + boundary-safe formatters + reproducible hardening evidence. 226-test coverage incl. hook layer (wrapper 94%, state 92%), lint-clean, pathlib-native. Centralized config + state. Adversarial review, CI/CD gates.",
"version": "4.4.0",
"description": "Enterprise Quality Engine — 16 skills, 17 commands, 16 hooks across 7 events. Opus 4.7 friction mitigation: false-positive-guard (Confidence-Drift), org-naming-pre-push (Wrong-Folder), ahead-of-remote-warning (Data-Loss), working-set-watch (Unversioned-Strategy). 346-test coverage incl. hook layer, lint-clean, pathlib-native. Centralized config + state. Adversarial review, CI/CD gates.",
"author": {
"name": "AI Engineering",
"email": "kontakt@ai-engineering.at"
Expand Down
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,70 @@
# Changelog

## v4.4.0 — 2026-05-01

Opus 4.7 friction mitigation — 4 new hooks addressing audit-2 trends
(+21% Wrong-Approach, +28% Buggy-Code, +22% Misunderstood-Request after
4.6→4.7 swap). Builds on the audit-2026-05-01 / Inkohärenz-Behebung work.

### Added — 4 New Hooks (12 → 16, +4 hooks across 4 events)

- **`hooks/false-positive-guard.py`** (UserPromptSubmit + PreToolUse Edit):
Detects Edit-tool invocations without bug-evidence in recent context.
UserPromptSubmit branch scans prompt for bug-keywords (DE+EN), stores
timestamp. PreToolUse Edit branch checks 10-min window; emits advisory
if no recent evidence. Mitigates 4.7's "false-positive bug invention"
pattern (audit-2 NEU). DoS-guard for 100KB+ inputs. 47 tests passed.

- **`hooks/org-naming-pre-push.py`** (PreToolUse Bash, matcher: git push):
Reads cwd's .git/config, parses GitHub org from origin URL, classifies
against allowlist (AI-Engineering-at, LEEI1337, FoxLabs-ai). Warns on
typo-org `AI-Engineerings-at` (server-redirected, but local URLs need
`git remote set-url`). Default mode: advisory only. 40 tests passed.

- **`hooks/ahead-of-remote-warning.py`** (SessionStart):
Iterates a watch-list (default: phantom-ai, nomos, zeroth, Playbook01,
wiki) and runs `git rev-list --count origin/<branch>..HEAD` (no fetch,
pure local). Emits advisory if any repo is ≥5 ahead, critical at ≥20.
Addresses nomos-97-unpushed pattern from today's audit. 13 tests passed.

- **`hooks/working-set-watch.py`** (SessionStart):
Scans `~/Downloads/` and `~/Documents/Downloads/` for strategy files
(Action_Plan*, DEC-*, *_Concept_*, Compliance_*, Lineage_*, M0*) older
than 7 days (warn) / 30 days (critical). Emits migration advisory
pointing at `zeroth/decisions/` or `zeroth/concepts/`. Addresses the
"Action Plan v1.0 lived unversioned in Downloads/" pattern. 20 tests
passed.

### Changed

- `hooks/hooks.json`: 13 → 18 hook commands (4 new files registered).
H1 false-positive-guard registered twice (UserPromptSubmit + PreToolUse
Edit). H2-H4 each register once.
- `.claude-plugin/plugin.json`: version 4.3.0 → 4.4.0.

### Tests

- 120 new tests across 4 hooks (47+40+13+20).
- All hooks ruff-clean (format + check).
- Pattern-Compliance review (Judge A): 7/9, blocker fixed before commit.
- Security/Performance review (Judge B): 3 critical findings (path-traversal,
race-condition in lib/state.py — separately filed; DoS-guard added in H1).

### Filed for Follow-up (out-of-scope, lib/state.py-wide)

- Path-Traversal: session_id not validated, allows `../etc/passwd`-style
escape from STATE_DIR. Affects all hooks. Filed in ERPNext.
- Race-Condition: concurrent `state.save()` in different hooks may
produce last-write-wins data loss. Filed in ERPNext.

### References

- Audit reports: `Documents/audit-2026-05-01/`
- Master fix list: `audit-2026-05-01/MASTER-FIX-EXECUTION.md`
- Plan: `~/.claude/plans/jaundder-plan-hashed-tiger.md`

---

## v4.3.0 — 2026-04-17/18

Hook-layer test coverage (+99 tests, 127→226) + session lessons +
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ P7 Context Recovery.
- **16 Skills**: creator, design, dispatch, doc-updater, feedback, git-worktrees, harden, init, judgment-day, knowledge, refactor-loop, statusbar, systematic-debugging, tdd, triad-review, verify
- **17 Commands**: /meta-audit, /meta-ci, /meta-create, /meta-design, /meta-discover, /meta-docs, /meta-feedback, /meta-harden, /meta-judgment, /meta-knowledge, /meta-loop, /meta-quality, /meta-snapshot, /meta-status, /meta-test, /meta-triad, /cancel-meta-loop
- **6 Agents**: doc-auditor, doc-editor, 3x doc-scanner, session-analyst
- **12 Hooks** across 7 events: session-start, session-init, correction-detect, scope-tracker, approach-guard, exploration-first, token-audit, quality-gate, context-recovery, meta-loop-stop, session-stop, session-end
- **16 Hooks** across 7 events: session-start, session-init, correction-detect, scope-tracker, approach-guard, exploration-first, token-audit, quality-gate, context-recovery, meta-loop-stop, session-stop, session-end, **false-positive-guard** (4.7 confidence-drift), **org-naming-pre-push** (Wrong-Folder), **ahead-of-remote-warning** (Data-Loss), **working-set-watch** (Unversioned-Strategy)

## Quality System

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ claude plugins list | grep meta-skills
meta-skills/
.claude-plugin/plugin.json # Plugin manifest (v4.0.0)
hooks/
hooks.json # 7 events, 12 hooks
hooks.json # 7 events, 16 hooks (v4.4.0)
lib/config.py # Centralized settings (all tunable values)
lib/services.py # Shared clients (Honcho, open-notebook, vault)
lib/hook_wrapper.py # Shared hook utilities
Expand Down
176 changes: 176 additions & 0 deletions hooks/ahead-of-remote-warning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Hook: ahead-of-remote-warning (SessionStart)

Mitigates "Ahead-of-Remote unentdeckt" pattern (audit: nomos had 97
unpushed commits before today's audit forced a push).

On SessionStart, iterates a configurable watch-list of repos and runs
`git rev-list --count origin/<branch>..HEAD` (NO fetch — uses local refs)
for each. Emits an advisory if any repo is ≥ warn threshold (5 ahead),
escalates if ≥ critical threshold (20).

Watch-list resolution order:
1. Env var AHEAD_WARN_WATCH (comma-separated absolute paths)
2. Default: phantom-ai, nomos, zeroth, Playbook01, wiki under
~/Documents/

Pure-local operation: no network calls, no `git fetch`. Won't update
remote refs. Hook exit 0 always; advisory only.
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))

from lib.state import SessionState

HOOK_NAME = "ahead_of_remote_warning"
WARN_THRESHOLD = 5
CRITICAL_THRESHOLD = 20
GIT_TIMEOUT_SECONDS = 5

DEFAULT_WATCH_DIRS = [
"phantom-ai",
"nomos",
"zeroth",
"Playbook01",
"wiki",
]


def classify_severity(count: int | None) -> str:
"""Return 'ok' | 'warn' | 'critical' | 'unknown'."""
if count is None or count < 0:
return "unknown"
if count >= CRITICAL_THRESHOLD:
return "critical"
if count >= WARN_THRESHOLD:
return "warn"
return "ok"


def _current_branch(repo: str) -> str | None:
"""Return current branch name, or None."""
try:
result = subprocess.run(
["git", "-C", repo, "symbolic-ref", "--short", "HEAD"],
capture_output=True,
text=True,
timeout=GIT_TIMEOUT_SECONDS,
)
if result.returncode == 0:
return result.stdout.strip() or None
except (OSError, subprocess.SubprocessError):
pass
return None


def count_ahead(repo: str, branch: str | None = None) -> int | None:
"""Count commits in `branch` that are not in `origin/<branch>`.

Returns None when the repo is invalid, has no origin, or the count
cannot be determined within the timeout.
"""
if not repo or not Path(repo, ".git").exists():
return None
if branch is None:
branch = _current_branch(repo)
if not branch:
return None
try:
result = subprocess.run(
["git", "-C", repo, "rev-list", "--count", f"origin/{branch}..HEAD"],
capture_output=True,
text=True,
timeout=GIT_TIMEOUT_SECONDS,
)
if result.returncode != 0:
return None
return int(result.stdout.strip())
except (OSError, ValueError, subprocess.SubprocessError):
return None


def _resolve_watch_list() -> list[str]:
"""Resolve watch-list of repo paths from env or defaults."""
env_value = os.environ.get("AHEAD_WARN_WATCH", "").strip()
if env_value:
return [p.strip() for p in env_value.split(",") if p.strip()]
home = Path.home()
base = home / "Documents"
return [str(base / name) for name in DEFAULT_WATCH_DIRS]


def _build_advisory(findings: list[dict]) -> str:
"""Build a single advisory string from the list of {repo, count, severity}."""
lines = [
"⚠️ Repos with unpushed commits (ahead-of-remote-warning):",
]
has_critical = any(f["severity"] == "critical" for f in findings)
for f in findings:
marker = "🔴" if f["severity"] == "critical" else "🟡"
repo_name = Path(f["repo"]).name
lines.append(f" {marker} {repo_name}: {f['count']} commits ahead ({f['severity']})")
if has_critical:
lines.append(
" → CRITICAL: ≥20 commits unpushed = data-loss risk on disk crash. "
"Push now (audit pattern: nomos had 97 unpushed before today's recovery)."
)
else:
lines.append(" → Run `git push` in each warned repo to clear advisory.")
lines.append("(Hook: ahead-of-remote-warning. Threshold: warn≥5, critical≥20.)")
return "\n".join(lines)


def main() -> None:
try:
raw = sys.stdin.read()
data = json.loads(raw) if raw.strip() else {}
except Exception:
sys.exit(0)

if not isinstance(data, dict):
sys.exit(0)
if data.get("hook_event_name") != "SessionStart":
sys.exit(0)

watch_list = _resolve_watch_list()
findings = []
for repo in watch_list:
count = count_ahead(repo)
severity = classify_severity(count)
if severity in ("warn", "critical"):
findings.append({"repo": repo, "count": count, "severity": severity})

if not findings:
sys.exit(0) # No repo at risk → silent

advisory = _build_advisory(findings)
print(json.dumps({"additionalContext": advisory}))

try:
session_id = data.get("session_id", "unknown")
state = SessionState(session_id)
ns = state.get(HOOK_NAME) or {}
import time as _time

ns["last_check_at"] = _time.time()
ns["repos_at_risk"] = [
{"repo": Path(f["repo"]).name, "count": f["count"], "severity": f["severity"]} for f in findings
]
state.set(HOOK_NAME, ns)
state.save()
except Exception:
pass

sys.exit(0)


if __name__ == "__main__":
main()
Loading
Loading