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
Empty file modified .github/hooks/post-checkout
100644 → 100755
Empty file.
Empty file modified .github/hooks/post-merge
100644 → 100755
Empty file.
1 change: 1 addition & 0 deletions .github/hooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ if [ -n "$STAGED_SRC" ]; then
[ -z "$file" ] && continue

# Already has an RCS header?
# shellcheck disable=SC2016 # $KYAULabs is a literal RCS marker, not a variable
if git show ":$file" 2>/dev/null | head -10 | grep -qF '$KYAULabs:'; then
continue
fi
Expand Down
5 changes: 1 addition & 4 deletions .github/hooks/pre-push
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ RESET=$'\033[0m'
# The git history serves as the development and evaluation log; squashing
# destroys the record of what was tried and abandoned.

remote="$1"
remote_url="$2"

while read -r local_ref local_oid remote_ref remote_oid; do
while read -r _local_ref local_oid _remote_ref remote_oid; do
# Skip tag pushes and branch deletions
if [ -z "$local_oid" ] || [ "$local_oid" = "0000000000000000000000000000000000000000" ]; then
continue
Expand Down
80 changes: 80 additions & 0 deletions .github/scripts/frontmatter-parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env node
// $KYAULabs: frontmatter-parser.js kyau@nova 2026/07/05 -0700 Exp $

// Extract a YAML frontmatter key's value from a Markdown file.
// Usage: node frontmatter-parser.js <file> <key>
// Prints the value to stdout (empty string if not found).
// Exits 1 on parse error.

'use strict';

const fs = require('fs');
const yaml = require('js-yaml');

const file = process.argv[2];
const key = process.argv[3];

if (!file || !key) {
console.error('Usage: node frontmatter-parser.js <file> <key>');
process.exit(2);
}

let content;
try {
content = fs.readFileSync(file, 'utf8');
} catch (e) {
console.error(`Error reading file: ${e.message}`);
process.exit(1);
}

// Extract frontmatter between first two --- lines
// Strip \r for CRLF safety, strip BOM (U+FEFF) for Windows editors
content = content.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');

const lines = content.split('\n');
if (lines[0] !== '---') {
// No frontmatter
process.stdout.write('');
process.exit(0);
}

let fmLines = [];
let foundClosing = false;
for (let i = 1; i < lines.length; i++) {
if (lines[i] === '---') {
foundClosing = true;
break;
}
fmLines.push(lines[i]);
}

if (!foundClosing) {
// No closing --- delimiter
process.stdout.write('');
process.exit(0);
}

const fmText = fmLines.join('\n');

let doc;
try {
doc = yaml.load(fmText);
} catch (e) {
console.error(`YAML parse error in ${file}: ${e.message}`);
process.exit(1);
}

if (doc === null || doc === undefined || typeof doc !== 'object') {
process.stdout.write('');
process.exit(0);
}

const value = doc[key];
if (value === undefined || value === null) {
process.stdout.write('');
} else {
process.stdout.write(String(value));
}

process.exit(0);
// vim: ft=javascript sts=4 sw=4 ts=4 noet :
27 changes: 8 additions & 19 deletions .github/scripts/install-hooks.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,24 @@
# $KYAULabs: install-hooks.sh kyau@nova 2026/07/04 -0700 Exp $

# Run once after cloning: bash .github/scripts/install-hooks.sh
#
# Uses git's native core.hooksPath mechanism instead of per-file symlinks.
# This avoids worktree crashes, core.hooksPath bypass, exec-bit dirtiness,
# and nullglob edge cases — all handled by git itself.

set -euo pipefail

REPO_ROOT=$(git rev-parse --show-toplevel)
HOOKS_SRC="$REPO_ROOT/.github/hooks"
GIT_HOOKS="$REPO_ROOT/.git/hooks"

if [ ! -d "$HOOKS_SRC" ]; then
echo "✗ .github/hooks directory not found at $HOOKS_SRC" >&2
exit 1
fi

echo "Installing git hooks from .github/hooks → .git/hooks"
git config core.hooksPath .github/hooks

for hook in "$HOOKS_SRC"/*; do
name=$(basename "$hook")
dest="$GIT_HOOKS/$name"
echo "✓ Hooks installed via core.hooksPath = .github/hooks"
echo " All commits will now run lint + gitleaks + commitlint."

if [ -f "$dest" ] && [ ! -L "$dest" ]; then
echo " ⚠ Backing up existing $name → $name.bak"
mv "$dest" "$dest.bak"
fi

ln -sf "$REPO_ROOT/.github/hooks/$name" "$dest"
chmod +x "$hook"
echo " ✓ $name"
done

echo ""
echo "✓ Hooks installed. All commits will now run lint + gitleaks + commitlint."

# vim: ft=sh sts=4 sw=4 ts=4 et :
# vim: ft=sh sts=4 sw=4 ts=4 et :
81 changes: 49 additions & 32 deletions .github/scripts/validate-harness.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,36 @@

set -euo pipefail

# ── Prerequisite: bash 4+ required for associative arrays ──────────────────────

if [ "${BASH_VERSINFO[0]:-0}" -lt 4 ]; then
echo "ERROR: Bash 4+ required (found ${BASH_VERSION:-unknown})" >&2
exit 1
fi

# ── Prerequisite: Node.js required for YAML frontmatter parsing ─────────────────

if ! command -v node >/dev/null 2>&1; then
echo "ERROR: Node.js required for YAML frontmatter parsing" >&2
exit 1
fi

# ── Configuration ────────────────────────────────────────────────────────────

HARNESS_DIR=".opencode"
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
if [ -z "$REPO_ROOT" ]; then
echo "ERROR: Not in a git repository. Must be run from within a git checkout." >&2
exit 1
fi

HARNESS_DIR="${REPO_ROOT}/.opencode"
SKILLS_DIR="${HARNESS_DIR}/skills"
AGENTS_DIR="${HARNESS_DIR}/agents"
COMMANDS_DIR="${HARNESS_DIR}/commands"

ERRORS=0
WARNINGS=0
NAME_REGISTRY="" # Track all names across categories to detect collisions
declare -A NAME_REGISTRY # key=name, value="file:category"

# ── Helpers ──────────────────────────────────────────────────────────────────

Expand All @@ -22,26 +42,20 @@ ok() { echo " OK: $*"; }

# Extract a YAML frontmatter key's value from a file.
# Usage: frontmatter_key <file> <key>
# Returns the value (whitespace trimmed) or empty string if not found.
# Returns the value or empty string if not found.
# Delegates to Node.js + js-yaml for proper YAML parsing (handles quoted
# values, folded scalars, block scalars, comments, and CRLF).
frontmatter_key() {
local file="$1" key="$2"
# Only search between the first pair of --- delimiters
awk -v key="$2" '
/^---$/ { in_fm=!in_fm; next }
in_fm && $1 == key ":" {
sub(/^[^:]+:[[:space:]]*/, "")
print
exit
}
' "$file" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
node "${REPO_ROOT}/.github/scripts/frontmatter-parser.js" "$file" "$key" 2>/dev/null || true
}

# Check that a file has paired --- frontmatter delimiters.
# Returns 0 if valid, 1 if not.
check_frontmatter_delimiters() {
local file="$1"
local open count
count=$(grep -c '^---$' "$file" 2>/dev/null || echo 0)
count=$(grep -c '^---$' "$file" 2>/dev/null) || count=0
if [ "$count" -eq 0 ]; then
return 1
fi
Expand All @@ -58,21 +72,19 @@ check_frontmatter_delimiters() {
}

# Register a name in the global registry and check for collisions.
# Uses associative array for O(1) exact-match lookup (no regex injection).
register_name() {
local name="$1" category="$2" file="$3"
if [ -z "$name" ]; then
return
fi
# Check if this name is already registered (any category)
local existing
existing=$(echo "$NAME_REGISTRY" | grep "^${name} " || true)
local existing="${NAME_REGISTRY[$name]:-}"
if [ -n "$existing" ]; then
local existing_file existing_cat
existing_file=$(echo "$existing" | awk '{print $2}')
existing_cat=$(echo "$existing" | awk '{print $3}')
local existing_file="${existing%%:*}"
local existing_cat="${existing##*:}"
err "${file}: name '${name}' already registered as ${existing_cat} in ${existing_file}"
else
NAME_REGISTRY="${NAME_REGISTRY}${name} ${file} ${category}"$'\n'
NAME_REGISTRY[$name]="${file}:${category}"
fi
}

Expand All @@ -90,7 +102,7 @@ check_required_field() {

echo "── Validating skills ──"
SKILL_COUNT=0
SKILL_NAMES=""
declare -A SKILL_NAMES

shopt -s nullglob
SKILL_FILES=( "${SKILLS_DIR}"/*/SKILL.md )
Expand Down Expand Up @@ -122,10 +134,10 @@ else

# Check for duplicate names within skills
if [ -n "$name" ]; then
if echo "$SKILL_NAMES" | grep -qx "$name"; then
if [ -n "${SKILL_NAMES[$name]:-}" ]; then
err "${skill_file}: duplicate skill name '${name}'"
else
SKILL_NAMES="${SKILL_NAMES}${name}"$'\n'
SKILL_NAMES[$name]=1
fi
register_name "$name" "skill" "$skill_file"
fi
Expand Down Expand Up @@ -213,12 +225,10 @@ echo "── Checking cross-references ──"
CROSSREF_COUNT=0

# Build a set of all known names from the registry
KNOWN_NAMES=""
while IFS= read -r line; do
[ -z "$line" ] && continue
name=$(echo "$line" | awk '{print $1}')
KNOWN_NAMES="${KNOWN_NAMES}${name}"$'\n'
done <<< "$NAME_REGISTRY"
declare -A KNOWN_NAMES
for name in "${!NAME_REGISTRY[@]}"; do
KNOWN_NAMES[$name]=1
done

# Scan explicit "Cross-refs" sections in skill files only.
# These sections list related skills/agents/commands by name — the structured
Expand Down Expand Up @@ -248,6 +258,7 @@ while IFS= read -r file; do

# Extract backtick-wrapped names from this line
# Use basic grep -o (no -P for portability)
# shellcheck disable=SC2016 # backticks are a literal grep pattern, not expansion
refs=$(echo "$line" | grep -o '`[^`]*`' 2>/dev/null || true)
if [ -z "$refs" ]; then
continue
Expand All @@ -270,8 +281,8 @@ while IFS= read -r file; do
"") continue ;;
esac

# Check against known names
if echo "$KNOWN_NAMES" | grep -qx "$ref_clean"; then
# Check against known names (associative array O(1) lookup)
if [ -n "${KNOWN_NAMES[$ref_clean]:-}" ]; then
CROSSREF_COUNT=$((CROSSREF_COUNT + 1))
else
warn "${file}: cross-refs unknown name '${ref}'"
Expand All @@ -282,10 +293,16 @@ done < <(find "${HARNESS_DIR}" -name 'SKILL.md' -not -path '*/node_modules/*' 2>

ok "${CROSSREF_COUNT} cross-reference(s) verified"

# ── Guard: fail on vacuous pass (all three categories empty) ──────────────────

if [ "$SKILL_COUNT" -eq 0 ] && [ "$AGENT_COUNT" -eq 0 ] && [ "$CMD_COUNT" -eq 0 ]; then
err "No skills, agents, or commands found — harness directory may be missing or empty"
fi

# ── Summary ──────────────────────────────────────────────────────────────────

echo ""
echo "═══════════════════════════════════════════════════════════════"
echo "═══════════════════════════════════════════════════════════"
if [ "$ERRORS" -eq 0 ] && [ "$WARNINGS" -eq 0 ]; then
echo "✓ Harness validation PASSED — 0 errors, 0 warnings"
echo "═══════════════════════════════════════════════════════════════"
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ jobs:
- name: Harness validation
run: bash .github/scripts/validate-harness.sh

- name: Shellcheck
run: shellcheck .github/scripts/*.sh .github/hooks/* tests/Shell/*.sh

- name: PHP CS Fixer (dry-run)
run: php vendor/bin/php-cs-fixer fix --dry-run --diff

Expand Down Expand Up @@ -98,6 +101,5 @@ jobs:
- name: Commitlint
if: github.event_name == 'pull_request'
run: |
echo "${{ github.event.pull_request.title }}" | npx commitlint --verbose
git fetch origin "${{ github.event.pull_request.base.ref }}"
npx commitlint --from="origin/${{ github.event.pull_request.base.ref }}" --to="HEAD" --verbose
4 changes: 2 additions & 2 deletions .opencode/commands/doctor.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ Floor: `semgrep` >= 1.168, `ocr` >= 1.7, `gitleaks` >= 8.30. These are run by ag
## 5. git hooks

```bash
if [ -f .git/hooks/pre-commit ]; then echo "INSTALLED"; else echo "NOT_INSTALLED — run 'bash .github/scripts/install-hooks.sh'"; fi
if [ -f .git/hooks/commit-msg ]; then echo "INSTALLED"; else echo "NOT_INSTALLED — run 'bash .github/scripts/install-hooks.sh'"; fi
hooks_path=$(git config core.hooksPath 2>/dev/null || echo "")
if [ "$hooks_path" = ".github/hooks" ]; then echo "INSTALLED ($hooks_path)"; else echo "NOT_INSTALLED — run 'bash .github/scripts/install-hooks.sh'"; fi
```

## Output
Expand Down
2 changes: 1 addition & 1 deletion .opencode/skills/brainstorming/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: brainstorming
description: Use before any creative work — creating features, building components, adding functionality, or modifying behavior. Refines rough ideas into a validated design through one-question-at-a-time grilling, then writes a spec. Hard-gate: no implementation until the design is approved and a spec is written (fast-path for zero-behavior-delta changes).
description: "Use before any creative work — creating features, building components, adding functionality, or modifying behavior. Refines rough ideas into a validated design through one-question-at-a-time grilling, then writes a spec. Hard-gate: no implementation until the design is approved and a spec is written (fast-path for zero-behavior-delta changes)."
derived-from: mattpocock/skills (MIT, © Matt Pocock)
---

Expand Down
2 changes: 1 addition & 1 deletion .opencode/skills/finding-duplicate-functions/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: finding-duplicate-functions
description: Use when seeking semantic duplication in a codebase — functions that solve the same problem for different callers. Complements /improve-architecture's deletion test. Two-phase: classical extraction then LLM intent-clustering.
description: "Use when seeking semantic duplication in a codebase — functions that solve the same problem for different callers. Complements /improve-architecture's deletion test. Two-phase: classical extraction then LLM intent-clustering."
derived-from: obra/superpowers-lab (MIT, © Jesse Vincent)
---

Expand Down
20 changes: 18 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ export default [
"no-unused-vars": "warn",
"no-console": "warn",
"indent": ["error", "tab"],
}
}
},
},
{
files: [".github/scripts/**/*.js"],
languageOptions: {
globals: {
require: "readonly",
process: "readonly",
console: "readonly",
__dirname: "readonly",
module: "readonly",
},
},
rules: {
"no-unused-vars": "warn",
"no-console": "off",
},
},
];
Loading
Loading