From 54b11a7189c71fed43109a52080422f3eafc0126 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Sat, 31 Oct 2026 08:39:40 -0400 Subject: [PATCH 1/6] feat: add agent skills to repo --- .skills/audit-code/SKILL.md | 310 ++++++++++++++ .skills/audit-skill/SKILL.md | 338 +++++++++++++++ .skills/audit-sys-prompt/SKILL.md | 218 ++++++++++ .skills/commit-push/SKILL.md | 415 ++++++++++++++++++ .skills/create-feature/SKILL.md | 672 ++++++++++++++++++++++++++++++ .skills/create-issue/SKILL.md | 565 +++++++++++++++++++++++++ .skills/fix-issue/SKILL.md | 301 +++++++++++++ .skills/git-tag/SKILL.md | 328 +++++++++++++++ .skills/purge-branches/SKILL.md | 106 +++++ .skills/restructure-code/SKILL.md | 373 +++++++++++++++++ .skills/scan-issues/SKILL.md | 235 +++++++++++ .skills/update-pr/SKILL.md | 242 +++++++++++ .skills/update-semver/SKILL.md | 396 ++++++++++++++++++ coverage.txt | 4 +- 14 files changed, 4501 insertions(+), 2 deletions(-) create mode 100644 .skills/audit-code/SKILL.md create mode 100644 .skills/audit-skill/SKILL.md create mode 100644 .skills/audit-sys-prompt/SKILL.md create mode 100644 .skills/commit-push/SKILL.md create mode 100644 .skills/create-feature/SKILL.md create mode 100644 .skills/create-issue/SKILL.md create mode 100644 .skills/fix-issue/SKILL.md create mode 100644 .skills/git-tag/SKILL.md create mode 100644 .skills/purge-branches/SKILL.md create mode 100644 .skills/restructure-code/SKILL.md create mode 100644 .skills/scan-issues/SKILL.md create mode 100644 .skills/update-pr/SKILL.md create mode 100644 .skills/update-semver/SKILL.md diff --git a/.skills/audit-code/SKILL.md b/.skills/audit-code/SKILL.md new file mode 100644 index 000000000..521690d5b --- /dev/null +++ b/.skills/audit-code/SKILL.md @@ -0,0 +1,310 @@ +--- +name: audit-code +description: Audits each directory in ./src for bugs, security vulnerabilities, and performance issues. Generates one consolidated issue per directory via /create-issue. +license: BSD 3-Clause +compatibility: Node.js project with ./src directory structure +metadata: + version: 2.3.0 + target: ./src + agent: code-review +--- + +# Audit Code + +> **⚠️ EXECUTION RULE:** Every code block in this skill is a shell command to **execute**. Do not print them as text, explain them, or treat them as examples — run them directly. + +Audit the `./src` directory tree for bugs, security vulnerabilities, and performance issues. Directories are independent — each gets its own issue. + +## State Management + +Use a state file to persist progress across responses. The state file is cleaned at the start of a new audit and at the end when all phases are complete. + +### State File Location + +The state file path is provided in the chain context (the text after `/audit-code`). If no path is provided, default to an example name like `audit-state.md` — the agent decides where to place it. + +To prevent collisions when multiple instances run, include a session identifier in the path: + +```bash +SESSION_ID=$(head -c 8 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' || echo $RANDOM$RANDOM$RANDOM) +STATE_FILE="${STATE_FILE_PATH:-audit-state-${SESSION_ID}.md}" +``` + +### State File Format + +```markdown +# Audit State + +## Phase Queue +- [x] ./src +- [ ] ./src/agent +- [ ] ./src/utils + +## Current Phase +./src/agent + +## Completed +- ./src + +## Findings +[Current phase findings stored here as markdown table] +``` + +### State Lifecycle + +1. **Start of audit** — Delete any existing state file at the configured path. Begin fresh. +2. **After each directory** — Save the current state (completed directories, current phase, findings). +3. **On resume** — Read the state file to determine where to pick up. +4. **End of audit** — Delete the state file once all phases are complete. + +**Cleanup:** The state file should exist only while the audit is actively in progress. Delete it once all phases are complete. If the audit fails or is interrupted, delete the state file as part of cleanup. Do not rely on a shell `trap` — the agent must explicitly remove its work files when the task finishes or when cleaning up after a failure. + +--- + +## Phase Protocol + +### Phase 0: Discovery + +1. **Derive repository info** once at the start (used for all issue operations): + ```bash + GIT_REMOTE=$(git remote get-url origin 2>/dev/null) + if echo "$GIT_REMOTE" | grep -q '^git@'; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\).git$/\1/') + elif echo "$GIT_REMOTE" | grep -q 'github\.com'; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\).git$/\1/') + else + echo "ERROR: Could not parse repository from remote '$GIT_REMOTE'." + exit 1 + fi + echo "REPO=$REPO" + ``` + +2. **Clean state.** Delete any existing state file: `rm -f "$STATE_FILE"` + +3. **Enumerate directories.** List all directories to audit: + ```bash + # List ./src and all immediate subdirectories, excluding common patterns + find ./src -maxdepth 1 -type d | sort + ``` + +4. Sort them alphabetically (the `find | sort` above handles this). + +5. Build the phase queue: `[./src, subdir1, subdir2, ...]` in alphabetical order. + +6. **Save the phase queue** to the state file: + ```bash + cat > "$STATE_FILE" << EOF + # Audit State + + ## Phase Queue + - [x] ./src + EOF + # Append subdirectories + find ./src -mindepth 1 -maxdepth 1 -type d | sort | while read dir; do + echo "- [ ] $dir" >> "$STATE_FILE" + done + echo "" >> "$STATE_FILE" + echo "## Current Phase" >> "$STATE_FILE" + echo "./src" >> "$STATE_FILE" + echo "" >> "$STATE_FILE" + echo "## Completed" >> "$STATE_FILE" + echo "- ./src" >> "$STATE_FILE" + echo "" >> "$STATE_FILE" + echo "## Findings" >> "$STATE_FILE" + ``` +6. **Proceed** to directory processing immediately. + +### Directory Processing + +Each directory is independent and gets its own issue, so there is no ordering dependency. + +#### Directory Scan Execution + +For each directory in the queue: + +1. **Read state.** Read the state file to get the current phase: + ```bash + CURRENT_PHASE=$(grep -A1 "^## Current Phase" "$STATE_FILE" | tail -1 | tr -d ' \n') + echo "Processing: $CURRENT_PHASE" + ``` + +2. **Enumerate files.** List all files in the target directory: + ```bash + # Recurse into subdirectories, excluding common patterns + find "$CURRENT_PHASE" -type f \ + ! -path '*/node_modules/*' \ + ! -path '*/.git/*' \ + ! -path '*/__tests__/*' \ + ! -path '*/.next/*' \ + ! -path '*/dist/*' \ + ! -path '*/build/*' \ + ! -name '*.test.*' \ + ! -name '*.spec.*' \ + ! -name '*.d.ts' \ + ! -name '*.js.map' \ + \( -name '*.js' -o -name '*.ts' -o -name '*.jsx' -o -name '*.tsx' \) + ``` + +3. **Audit each file.** For each file, check for issues using concrete patterns: + + **Bugs** — grep for common anti-patterns: + ```bash + # Silent catch blocks + grep -rn 'catch\s*(' "$file" | grep -v 'err' | grep -v 'error' + # Unhandled promises (no .catch()) + grep -rn '\.then(' "$file" | grep -v '\.catch(' + # console.log in production code + grep -rn 'console\.log' "$file" + ``` + + **Security** — grep for OWASP Top 10 violations (per AGENTS.md §1.2): + ```bash + # Hardcoded secrets — capture count only, NEVER print values to issue body + SECRET_COUNT=$(grep -rnc 'password\s*=\s*["\x27]' "$file" 2>/dev/null | awk -F: '{s+=$NF}END{print s}') + SECRET_COUNT=${SECRET_COUNT:-0} + # eval() usage (forbidden per AGENTS.md §1.1) + grep -rn 'eval(' "$file" + # SQL injection vectors (string concatenation in queries) + grep -rn "query\s*.*\+\s*req" "$file" + # Missing auth middleware on routes + # (check route definitions for missing auth guards) + ``` + + **CRITICAL:** When writing security findings to the issue body or state file, NEVER include the matched line content. Only report the pattern type and count: + + ``` + | [file] | security | critical | Potential hardcoded secret: password/api_key pattern detected (N occurrences) | + ``` + + The actual secret value must never appear in any output — it will be written to a GitHub issue via `gh issue edit`, exposed to anyone with repo access. + + **Performance** — grep for common issues: + ```bash + # Synchronous file I/O in async functions + grep -rn 'readFileSync\|writeFileSync' "$file" + # Unbounded loops + grep -rn 'while\s*(true)' "$file" + # Large bundle imports + grep -rn 'import.*\*' "$file" + ``` + +4. **Classify severity.** Use these concrete criteria: + - **Critical:** Data breach risk, production crash, data loss, authentication bypass + - **High:** Functional bug affecting users, XSS, SQL injection, memory leak + - **Medium:** Performance degradation, missing error handling, code smell + - **Low:** Minor optimization, style inconsistency, TODO items + + **Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to classify each finding's severity. Issue one `choice` question per finding, passing the finding as state. + + ``` + Question: "Classify the severity of this finding: [finding description]" + Type: choice + Options: critical, high, medium, low + Criteria: + critical = data breach risk, production crash, data loss, authentication bypass + high = functional bug affecting users, XSS, SQL injection, memory leak + medium = performance degradation, missing error handling, code smell + low = minor optimization, style inconsistency, TODO items + Example answer: high + ``` + + Interpret the returned label directly. Fall back to the criteria above if no decision tool is available. + +5. **Create Issue.** Invoke the `create-issue` skill with a synthesized description: + ``` + create-issue Audit findings for $CURRENT_PHASE: Found N critical, N high, N medium, N low issues. See audit results below for details. + ``` + + **Capture `ISSUE_NUMBER` from the create-issue output** (the chained skill prints `ISSUE_NUMBER=`): + + ```bash + # Capture ISSUE_NUMBER from the conversation history (last occurrence wins) + ISSUE_NUMBER=$(echo "$CONVERSATION_HISTORY" | grep -oE '^ISSUE_NUMBER=[0-9]+' | tail -1 | cut -d= -f2) + if [ -z "$ISSUE_NUMBER" ]; then + echo "ERROR: Could not capture ISSUE_NUMBER from create-issue output. Conversation history:" + echo "$CONVERSATION_HISTORY" | tail -20 + exit 1 + fi + echo "ISSUE_NUMBER=$ISSUE_NUMBER" + ``` + + **After creating the issue (or failing to), continue to Step 6.** Do not stop or wait for further input — the pipeline proceeds automatically. + +6. **Update Issue Body.** Append a structured audit table to the issue. Collect findings into a temp file, then write them to the issue: + + ```bash + AUDIT_FILE=$(mktemp) + + # Write the audit table header + printf "| File | Type | Severity | Summary |\n|------|------|----------|---------|\n" > "$AUDIT_FILE" + + # Append each finding as a table row (populated from the audit results collected above) + # Example: printf "| src/foo.js | bug | high | Unhandled promise rejection |\n" >> "$AUDIT_FILE" + + gh issue edit "$ISSUE_NUMBER" --body-file "$AUDIT_FILE" --repo "$REPO" + + # Verify the body was updated successfully + VERIFY_BODY=$(gh issue view "$ISSUE_NUMBER" --json body --jq '.body' --repo "$REPO" 2>/dev/null || true) + if ! echo "$VERIFY_BODY" | grep -q "File.*Type.*Severity.*Summary"; then + echo "WARNING: Failed to verify audit table was written to issue #$ISSUE_NUMBER." + else + echo "Audit table verified on issue #$ISSUE_NUMBER." + fi + + rm -f "$AUDIT_FILE" + ``` + +7. **Update State.** Mark the directory as completed in the state file: + ```bash + # Use | as sed delimiter to safely handle directory paths containing / + ESCAPED_PHASE=$(echo "$CURRENT_PHASE" | sed 's/[|&]/\\&/g') + sed -i "s|- \[ \] $ESCAPED_PHASE|- [x] $ESCAPED_PHASE|" "$STATE_FILE" + # Append findings + echo "" >> "$STATE_FILE" + echo "## Findings: $CURRENT_PHASE" >> "$STATE_FILE" + echo "[Findings stored here]" >> "$STATE_FILE" + ``` + +8. **Report Output.** Use the standard Output Format below. + +9. **Advance.** Move to the next directory in the queue. If none remain, delete the state file (`rm -f "$STATE_FILE"`) and report completion. + +--- + +## Execution Rules + +1. **STATE PERSISTENCE.** Always save state after processing each directory. Always read state before starting a new directory. +2. **ONE ISSUE PER DIRECTORY.** Do not combine multiple directories into a single issue. +3. **STOP CONDITION.** Terminate when all directories in the phase queue have been processed. + +## Error Handling + +- **State file not found:** If the state file doesn't exist on resume, begin fresh from Phase 0. +- **Directory not found:** If a directory in the phase queue doesn't exist, skip it and log a warning. +- **gh CLI not authenticated:** Suggest running `gh auth login`. +- **Network error:** Retry once, then report the failure and skip the current directory. +- **create-issue failure:** Log the error, skip issue creation for this directory, and continue. Do not abort the audit. +- **Partial audit:** If some directories succeed and others fail, report what was accomplished. Never leave the audit half-done. +- **Rate limit:** If GitHub API returns 403 rate limit, wait 60 seconds and retry once. + +## Gotchas + +- **Security findings must never include raw secret values.** The audit table only reports pattern type and count — never the matched line content. Writing secrets to a GitHub issue exposes them to anyone with repo access. +- **State file cleanup is critical.** Always delete the state file at the start of a new audit and at the end when complete. Stale state files cause incorrect resume behavior. +- **`gh` CLI must be authenticated before starting.** The audit will fail silently at Step 6 if `gh` is not logged in. Run `gh auth login` first. + +## Output Format + +Each phase response must include: + +``` +## Audit: [directory path] +- **Status:** completed | blocked | no-issues +- **Files Audited:** [count] +- **Issues Found:** [count] (critical: N, high: N, medium: N, low: N) +- **Details:** + - [key-finding-1] + - [key-finding-2] +- **Action Items:** [issue created with ID or "No issues detected"] +- **Next Steps:** [next directory name or "All phases complete"] +``` \ No newline at end of file diff --git a/.skills/audit-skill/SKILL.md b/.skills/audit-skill/SKILL.md new file mode 100644 index 000000000..027aa610f --- /dev/null +++ b/.skills/audit-skill/SKILL.md @@ -0,0 +1,338 @@ +--- +name: audit-skill +description: Audits Agent Skills SKILL.md files against the protocol specification and best practices. Identifies issues across 10 categories: schema violations, naming, description quality, structure, progressive disclosure, file references, best practices, calibration, content quality, and optional directories. Produces a structured report with severity levels and actionable fixes. +license: BSD 3-Clause +compatibility: Requires Node.js 24+. Reads SKILL.md files from any directory. No network access needed. +metadata: + version: 1.0.0 + category: quality-assurance + agent: documentation +--- + +# Audit Skill + +Audits Agent Skills `SKILL.md` files against the Agent Skills specification and best practices. Produces a structured report with severity levels and actionable fixes. + +**Note:** This skill operates offline. Do not rely on external URLs for validation — use the spec constraints defined below. + +## Usage + +Pass the path to the skill directory as a chain instruction: + +``` +audit-skill /path/to/skill-directory +``` + +The skill path is the first argument after `audit-skill` in the instruction. The agent reads the SKILL.md from that directory and produces a structured audit report. + +## Audit Categories + +Each skill is checked against 10 categories. Every finding includes a severity level: + +| Severity | Meaning | +|----------|---------| +| **ERROR** | Protocol violation — will fail validation | +| **WARN** | Best practice violation — likely to cause problems | +| **INFO** | Suggestion — optional improvement | + +### 1. Schema Violations (ERROR) + +Check frontmatter fields against spec constraints: + +- **`name`**: Required. Max 64 characters. Non-empty. +- **`description`**: Required. Max 1024 characters. Non-empty. +- **`compatibility`**: Optional. Max 500 characters if provided. +- **`metadata`**: Optional. Must be a string-to-string map (no nested objects) per the standard spec. If the project uses an extended spec that allows nested metadata, skip this check. +- **`license`**: Optional. No length constraint, but keep it short. + +**Note:** `allowed-tools` is NOT part of the standard agent skills spec. If present, log it as INFO (non-standard field) but do not flag as ERROR. + +Parse the YAML frontmatter and validate each field. Report any constraint violations. + +### 2. Naming Issues (ERROR) + +Validate the `name` field against naming rules: + +- Must be 1–64 characters +- Only lowercase alphanumeric (`a-z`, `0-9`) and hyphens (`-`) +- Must not start or end with a hyphen +- Must not contain consecutive hyphens (`--`) +- Must match the parent directory name + +Check both the frontmatter `name` field and the actual directory name. Report mismatches. + +### 3. Description Quality (WARN) + +Evaluate the `description` field for quality: + +- **Too short**: Less than 20 characters — likely insufficient +- **Too long (WARN)**: Over 800 characters — consider trimming for readability +- **Too long (ERROR)**: Over 1024 characters — exceeds spec maximum +- **Missing "when to use"**: Should describe both what the skill does AND when to use it. Look for phrases like "use when", "for", "handles", "processes". A description that only says what it does without triggering context is weak. +- **Vague language**: Contains generic phrases like "helps with", "deals with", "manages" without specific context +- **Missing keywords**: Should include specific domain keywords that help agents identify relevant tasks + +Good description pattern: `[Action] [domain]. Use when [triggering context].` + +### 4. Structure Issues (WARN) + +Check the Markdown body organization: + +- **No heading**: Body should start with a heading (`# Title`) +- **No sections**: Should have at least 2 subheadings (`## Section`) for navigability +- **No examples**: Should include at least one code example or usage example +- **No step-by-step**: Should have numbered steps or a clear workflow for non-trivial skills +- **No edge cases**: Should mention common edge cases or error conditions + +A well-structured skill has: title, overview, steps/workflow, examples, edge cases/gotchas. + +### 5. Progressive Disclosure (WARN) + +Check for proper use of progressive disclosure: + +- **Too large**: SKILL.md exceeds or equals 500 lines (>= 500) — should split into references +- **Token-heavy**: If body exceeds ~5000 tokens, recommend splitting +- **Missing references**: If the skill has detailed content (scripts, templates, schemas), check for `references/` or `assets/` directory usage +- **No conditional loading**: If using references, should tell the agent *when* to load them (e.g., "Read `references/api-errors.md` if the API returns a non-200 status") + +A skill under 200 lines rarely needs progressive disclosure. Flag only when content is substantial but not properly split. + +### 6. File Reference Issues (ERROR/WARN) + +Check file references within the SKILL.md: + +- **Broken references (ERROR)**: References to files in `references/`, `assets/`, or `scripts/` that don't exist +- **Deep nesting (WARN)**: References more than one level deep (e.g., `references/subdir/file.md`) — spec recommends one level deep +- **Relative paths**: All references should be relative to the skill root +- **Missing referenced files**: If the body mentions a script or reference, verify it exists + +Check for markdown link syntax `[text](path)` and code block references. Use `ls` to verify file existence. + +### 7. Best Practices (WARN/INFO) + +Check against best practices from the spec: + +- **Missing gotchas**: Non-trivial skills should have a "Gotchas" or "Common pitfalls" section +- **Missing templates**: If the skill produces structured output, should include a format template +- **Missing checklists**: Multi-step workflows should have a checklist +- **Missing validation**: Complex skills should include a validation step +- **No scripts for repeated logic**: If the skill describes the same logic repeatedly, consider bundling a script in `scripts/` +- **No defaults**: When multiple approaches exist, should pick a default and mention alternatives briefly + +### 8. Calibration Issues (WARN) + +Check instruction specificity: + +- **Too vague**: Instructions like "handle errors appropriately", "follow best practices", "use good judgment" without specifics. Flag if > 10% of instructions are vague. +- **Too prescriptive**: Overly rigid instructions for tasks that tolerate variation (e.g., "always use X" when Y is equally valid) +- **No why explanations**: Flexible instructions should explain *why*, not just *what*. Flag if > 20% of instructions lack rationale. +- **Menus instead of defaults**: Lists multiple tools/approaches as equal options without a recommended default +- **Procedure vs declaration**: Skill teaches *what to produce* for a specific instance instead of *how to approach* a class of problems + +Concrete example of vague instruction: "Make sure the code is good" → Should specify: "Follow AGENTS.md §3.3 error handling rules and §3.5 testing conventions." + +**Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to judge whether an instruction is too vague to be actionable. Issue one `noul` question per instruction, passing the instruction as state. + +``` +Question: "Is this instruction too vague to be actionable?" +Type: noul +Criteria: + false = references a concrete file, rule, or pattern + true = no specific action or rule referenced +Example answer: true +``` + +**Important:** The `noul` type returns a *single float in [0,1]*, not a label — it is the probability of `true`. Interpret it as: `≥ 0.6` → true (vague), `≤ 0.4` → false (actionable), between → unknown. Fall back to the calibration criteria above if no decision tool is available. + +### 9. Content Quality (WARN) + +Check for domain-specific expertise: + +- **Generic instructions**: Relies on general knowledge instead of project-specific or domain-specific context. Flag if > 30% of content is generic. +- **Missing domain context**: Doesn't include API patterns, edge cases, or project conventions +- **Explains the obvious**: Describes basic concepts the agent already knows (e.g., "PDF files are a common file format"). Flag if > 15% of content explains obvious concepts. +- **No real-world examples**: Lacks concrete examples from actual usage + +A good skill adds what the agent lacks and omits what it already knows. Concrete example: Instead of "Handle errors", say "Catch `ENOENT` when reading config files and fall back to defaults." + +### 10. Optional Directories (INFO) + +Check usage of optional directories: + +- **`scripts/` present**: Verify scripts are self-contained, executable, and documented. Flag if scripts reference files outside the skill directory. +- **`references/` present**: Verify reference files are focused (under 200 lines each) and not too large. Flag if any reference exceeds 500 lines. +- **`assets/` present**: Verify assets are templates, images, or data files (not code). Flag if assets contain executable code. +- **Missing opportunities**: If the skill has large templates or repeated logic, suggest `scripts/` or `assets/` +- **Empty directories**: Flag if `scripts/`, `references/`, or `assets/` exist but are empty — remove them or populate them. + +## Execution + +1. **Read the SKILL.md** from the provided skill directory: + ```bash + cat "$SKILL_PATH/SKILL.md" + ``` +2. **Parse the YAML frontmatter** — extract all fields using a YAML parser: + ```bash + # Extract frontmatter (between first and second ---) + sed -n '1,/^---$/p' "$SKILL_PATH/SKILL.md" | tail -n +2 | head -n -1 + ``` + Parse the YAML frontmatter to extract fields. If no YAML parser is available, parse manually with `grep`/`sed`. +3. **Read the Markdown body** — analyze structure and content: + ```bash + # Count lines in body (after frontmatter) + sed '1,/^---$/d' "$SKILL_PATH/SKILL.md" | wc -l + # Count subheadings + grep -c '^## ' "$SKILL_PATH/SKILL.md" || true + # Count code blocks (opening ``` lines, divide by 2 for block count) + BLOCK_COUNT=$(grep -c '^```' "$SKILL_PATH/SKILL.md" 2>/dev/null || echo 0) + echo "Code blocks: $((BLOCK_COUNT / 2))" + ``` +4. **Check for optional directories** — list `scripts/`, `references/`, `assets/` if they exist: + ```bash + for dir in scripts references assets; do + if [ -d "$SKILL_PATH/$dir" ]; then + echo "$dir/ exists ($(ls -1 "$SKILL_PATH/$dir" 2>/dev/null | wc -l) files)" + else + echo "$dir/ — not present" + fi + done + ``` +5. **Validate file references** — check that referenced files exist: + ```bash + # Extract file paths from markdown links and code references + grep -oE '\[[^]]*\]\([^)]*\)' "$SKILL_PATH/SKILL.md" | sed 's/.*(\([^)]*\))/\1/' | while read ref; do + if [ ! -f "$SKILL_PATH/$ref" ]; then + echo "BROKEN: $ref" + fi + done + ``` +6. **Apply all 10 category checks** — collect findings +7. **Produce a structured report** — grouped by category, sorted by severity. Skip categories with no findings. + +## Output Format + +Produce a structured report in this format: + +``` +## Skill Audit: +- **Path:** +- **Status:** +- **Findings:** total ( errors, warnings, info) + +### Schema Violations +- [ERROR] description exceeds 1024 characters (found: 1203) + +### Naming Issues +- [WARN] name contains consecutive hyphens: "my--skill" + +### Description Quality +- [WARN] description is only 12 characters — consider expanding +- [INFO] missing "when to use" context + +### Structure Issues +- [WARN] no examples found — add at least one usage example + +### Progressive Disclosure +- [INFO] skill is 45 lines — no splitting needed + +### File References +- [ERROR] references `scripts/extract.py` but file does not exist + +### Best Practices +- [WARN] no gotchas section — consider adding common pitfalls + +### Calibration +- [WARN] vague instruction: "handle errors appropriately" + +### Content Quality +- [INFO] explains basic concepts the agent likely already knows + +### Optional Directories +- [INFO] no scripts/ directory — consider bundling repeated logic + +### Summary +- **Severity breakdown:** 2 errors, 4 warnings, 3 info +- **Top priority fixes:** [list top 3 actionable items] +``` + +**Note:** Skip categories with no findings. Only include sections that have at least one finding. + +## Heuristics Reference + +Use these concrete thresholds: + +| Check | Threshold | Severity | +|-------|-----------|----------| +| Name length | > 64 chars | ERROR | +| Description length | < 20 chars | WARN | +| Description length | > 800 chars | WARN | +| Description length | > 1024 chars | ERROR | +| Compatibility length | > 500 chars | ERROR | +| SKILL.md lines | > 500 lines | WARN | +| Subheadings | < 2 | WARN | +| Examples | 0 found | WARN | +| File references | Broken | ERROR | +| Reference depth | > 1 level | WARN | + +## Error Handling + +- **SKILL.md not found:** Report the error and stop. No audit possible. +- **Invalid YAML frontmatter:** Report the parse error and stop. +- **Directory not found:** Report the error and stop. +- **Permission denied:** Report the specific file/directory and stop. Do not attempt to bypass permissions. +- **Partial audit:** If some checks fail but others succeed, report what was accomplished. Never leave the audit half-done. + +## Example + +``` +User: audit-skill /home/jason/Projects/madz/skills/my-skill + +Agent: Auditing skill "my-skill"... + +## Skill Audit: my-skill +- **Path:** /home/jason/Projects/madz/skills/my-skill +- **Status:** warnings +- **Findings:** 5 total (0 errors, 4 warnings, 1 info) + +### Description Quality +- [WARN] description is only 15 characters — consider expanding +- [INFO] missing "when to use" context + +### Structure Issues +- [WARN] no examples found — add at least one usage example +- [WARN] only 1 subheading — add more sections for navigability + +### Best Practices +- [WARN] no gotchas section — consider adding common pitfalls + +### Progressive Disclosure +- [INFO] skill is 30 lines — no splitting needed + +### Summary +- **Severity breakdown:** 0 errors, 4 warnings, 1 info +- **Top priority fixes:** + 1. Expand description to at least 20 characters with "when to use" context + 2. Add at least one usage example + 3. Add a gotchas section for common pitfalls +``` + +### Error Case Example + +``` +User: audit-skill /home/jason/Projects/madz/skills/nonexistent-skill + +Agent: Auditing skill "nonexistent-skill"... + +## Skill Audit: nonexistent-skill +- **Path:** /home/jason/Projects/madz/skills/nonexistent-skill +- **Status:** error +- **Findings:** 1 total (1 errors, 0 warnings, 0 info) + +### Schema Violations +- [ERROR] SKILL.md not found at /home/jason/Projects/madz/skills/nonexistent-skill/SKILL.md + +### Summary +- **Severity breakdown:** 1 errors, 0 warnings, 0 info +- **Top priority fixes:** + 1. Verify the skill path is correct and the SKILL.md file exists +``` \ No newline at end of file diff --git a/.skills/audit-sys-prompt/SKILL.md b/.skills/audit-sys-prompt/SKILL.md new file mode 100644 index 000000000..821d7a4cd --- /dev/null +++ b/.skills/audit-sys-prompt/SKILL.md @@ -0,0 +1,218 @@ +--- +name: audit-sys-prompt +description: Expert System Prompt Evaluator Agent. Analyzes, rates, and provides actionable feedback for system prompts against a 7-criteria framework. Returns structured machine-readable JSON output with weighted scores, evidence-based reasoning, and harness-aware recommendations. +license: BSD 3-Clause +compatibility: Requires a capable LLM for evaluation. Designed for AI harness environments with tool calling, memory layers, and guardrails. +metadata: + agent: documentation +--- + +# Audit System Prompt + +You are an **Expert System Prompt Evaluator Agent**. Your sole purpose is to analyze, rate, and provide actionable feedback for system prompts used in AI harness environments. + +You evaluate prompts against a standardized 7-criteria framework, return structured machine-readable output, and maintain strict neutrality, evidence-based reasoning, and harness-aware awareness. + +--- + +## Source + +Read the system prompt file. The path is provided in the chain context (the text after `/audit-sys-prompt`). If no path is provided, try these locations in order: + +1. `./prompts/SYSTEM_PROMPT.md` +2. `./SYSTEM_PROMPT.md` +3. `.opencode/system_prompt.md` + +```bash +PROMPT_FILE="${PROMPT_FILE_PATH:-}" +if [ -z "$PROMPT_FILE" ]; then + # No explicit path — try defaults + for candidate in "./prompts/SYSTEM_PROMPT.md" "./SYSTEM_PROMPT.md" ".opencode/system_prompt.md"; do + if [ -f "$candidate" ]; then + PROMPT_FILE="$candidate" + break + fi + done +fi + +if [ -z "$PROMPT_FILE" ] || [ ! -f "$PROMPT_FILE" ]; then + echo '{"error":"File not found","message":"Could not read system prompt file"}' + exit 1 +fi + +cat "$PROMPT_FILE" +``` + +--- + +## Evaluation Criteria & 1–5 Anchors + +Rate each criterion on a strict 1–5 scale. Base scores on **explicit textual evidence** from the prompt. Use default weights unless overridden. + +### 1. role_clarity (Default weight: 0.18) + +- **1** = Vague, conflicting, or missing role/scope +- **2** = Somewhat clear but vague on scope or audience +- **3** = Clear but lacks audience definition or success metrics +- **4** = Clear role and audience, but missing one element (success metrics or bounded scope) +- **5** = Explicit role, target audience, core objective, and success definition with bounded scope + +### 2. constraints (Default weight: 0.15) + +- **1** = Open-ended, encourages speculation, no negative constraints +- **2** = Some limits but very loose, no fallbacks +- **3** = Has limits but lacks fallback behaviors or scope guards +- **4** = Strong limits and fallbacks, but missing one element (cutoffs, boundaries, or degradation rules) +- **5** = Explicit positive/negative constraints, knowledge cutoffs, hard boundaries, and graceful degradation rules + +### 3. output_format (Default weight: 0.15) + +- **1** = No format guidance, freeform, machine-parsing fails +- **2** = Basic format mentioned but inconsistent or unvalidated +- **3** = Basic structure requested but lacks validation hooks or schema alignment +- **4** = Mostly strict format with validation, but lacks one element (consistent across turns or schema alignment) +- **5** = Strict, deterministic format spec (JSON/Markdown/sections), includes validation-ready keys, consistent across turns + +### 4. safety_compliance (Default weight: 0.17) + +- **1** = Unsafe defaults, ignores policy, no refusal logic +- **2** = Some safety awareness but generic, no specific triggers +- **3** = Generic safety rules but lacks actionable triggers or PII handling +- **4** = Strong safety rules with triggers and PII handling, but missing one element (audit logging or policy alignment) +- **5** = Explicit refusal paths, bias mitigation, PII/data privacy rules, audit-ready logging hooks, aligns with organizational policy + +### 5. robustness (Default weight: 0.15) + +- **1** = Fragile, hallucinates on ambiguity, breaks on minor phrasing +- **2** = Handles obvious cases but fragile on edge cases +- **3** = Handles common cases but lacks clarification requests or error fallbacks +- **4** = Strong error handling and clarification, but lacks one element (adversarial resistance or stress testing) +- **5** = Proactive clarification prompts, structured error handling, stays in bounds under stress, consistent adversarial resistance + +### 6. tone_consistency (Default weight: 0.10) + +- **1** = Inconsistent voice, unprofessional, shifts style unpredictably +- **2** = Generally consistent but noticeable drift +- **3** = Generally aligned but drifts on complex or multi-turn prompts +- **4** = Stable and calibrated, but not fully turn-stable or audience-perfect +- **5** = Stable, audience-appropriate, calibrated expertise, turn-stable behavior + +### 7. harness_integration (Default weight: 0.10) + +- **1** = Ignores harness capabilities, fights with state/routing +- **2** = Mentions tools/memory but no clear invocation or state awareness +- **3** = Mentions tools/memory but lacks clear invocation syntax or state awareness +- **4** = Good tool-call syntax and routing, but missing one element (error recovery or pipeline optimization) +- **5** = Explicit tool-call syntax, memory references, routing instructions, error recovery, optimized for harness pipeline + +--- + +## Scoring & Weighting Rules + +**Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to rate each criterion. Issue one `score` question per criterion, passing the prompt text as state. Provide the rubric anchors as an **ordered array** (lowest to highest). + +``` +Question: "Rate role_clarity based on explicit textual evidence." +Type: score +Levels (ordered, lowest to highest): + 0 = Vague, conflicting, or missing role/scope + 1 = Somewhat clear but vague on scope or audience + 2 = Clear but lacks audience definition or success metrics + 3 = Clear role and audience, but missing one element + 4 = Explicit role, target audience, core objective, and success definition with bounded scope +Example answer: 3 +``` + +**Important:** The `score` type returns a **probability-weighted average of the zero-based levels** — a continuous value on a `0..(N-1)` scale, not a discrete 1-N level. For a 5-level rubric, the returned score is on a 0–4 scale; **add 1 to map it to the 1–5 rubric**, then round to the nearest level. Do not trust the argmax index, which can disagree with the rounded weighted average. If no decision tool is available, rate manually using the anchors. + +1. **Calculate weighted score:** `Σ(criteria_score × weight)` +2. **Round to 1 decimal place** +3. **Pass threshold:** `≥ 4.2 overall AND no single criterion ≤ 3.0` +4. **Custom weights:** If `custom_weights` provided, replace default weights with custom values for specified keys. Then normalize the complete set of 7 weights so they sum to 1.0 using: `normalized_weight = custom_or_default_weight / Σ(all_7_weights)`. Invalid keys are ignored with a WARN logged. +5. **Missing context:** If `use_case_context` or `harness_capabilities` are missing, assume standard defaults but note this in `rationale` + +--- + +## Output Schema + +Return **ONLY valid JSON**. Do not include markdown, explanations, or extra text. The schema below is for reference only. + +```json +{ + "prompt_id": "", + "weighted_total_score": , + "meets_pass_threshold": , + "criteria_ratings": { + "role_clarity": {"score": , "weight_used": , "evidence": ["", ...]}, + "constraints": {"score": , "weight_used": , "evidence": ["", ...]}, + "output_format": {"score": , "weight_used": , "evidence": ["", ...]}, + "safety_compliance": {"score": , "weight_used": , "evidence": ["", ...]}, + "robustness": {"score": , "weight_used": , "evidence": ["", ...]}, + "tone_consistency": {"score": , "weight_used": , "evidence": ["", ...]}, + "harness_integration": {"score": , "weight_used": , "evidence": ["", ...]} + }, + "rationale": "", + "improvements": [ + "", + "", + "" + ], + "harness_recommendations": [ + "" + ], + "confidence_score": +} +``` + +**Confidence score rubric:** +- **1.0** = Prompt is complete, unambiguous, and fully evaluable (no truncation, all sections present) +- **0.8** = Minor gaps in context but sufficient for evaluation (e.g., missing one criterion section) +- **0.6** = Truncated or missing significant context (prompt cut off mid-section, > 20% missing) +- **0.4** = Severely incomplete; evaluation is speculative (prompt cut off early, > 50% missing) + +**Prompt size handling:** If the prompt exceeds 50,000 characters, truncate to the first 50,000 characters and set `confidence_score ≤ 0.8`. Note the truncation in `rationale`. + +--- + +## Operational Constraints + +- **Cite exact phrases** from the input prompt as evidence. Do not invent constraints. +- **Remain neutral and objective.** Avoid subjective praise/criticism. +- **Truncated prompts:** If the prompt is severely truncated or missing context, state this in `rationale` and set `confidence_score ≤ 0.6`. +- **Never modify the original prompt.** Only analyze and suggest. +- **Output must be strictly valid JSON.** Fail gracefully with a parseable error JSON if input is invalid. +- **Validate output:** Before returning, verify the JSON is valid (no trailing commas, all keys quoted, proper escaping). If validation fails, regenerate. +- **Custom weights validation:** If `custom_weights` contains invalid keys (not in the 7 criterion keys), log a WARN and ignore the invalid keys. Do not abort. +- **Multi-document YAML:** If the system prompt contains YAML frontmatter, strip it before evaluation. The prompt body is the content after the frontmatter. + +## Gotchas + +- **JSON output must be strictly valid.** The agent must validate the JSON before returning — no trailing commas, all keys quoted, proper escaping. If validation fails, regenerate. +- **Custom weights must sum to 1.0 after normalization.** Invalid keys are silently ignored with a WARN, but the remaining weights are auto-normalized. +- **Truncated prompts degrade confidence.** If the prompt exceeds 50,000 characters, set `confidence_score ≤ 0.8` and note the truncation in `rationale`. + +## Usage + +No input required. The skill reads the system prompt file automatically (see **Source** section) and returns a structured evaluation report. + +### Optional Parameters + +These may be provided in the chain context or as environment variables: + +- **`prompt_id`**: A short string identifier for this prompt. If omitted, a hash of the first 200 characters is used (via `sha256sum` or `shasum -a 256` depending on platform). +- **`custom_weights`**: Override criterion weights as a JSON object. Example: + + ```json + { + "role_clarity": 0.20, + "safety_compliance": 0.25, + "output_format": 0.10 + } + ``` + + Valid keys: `role_clarity`, `constraints`, `output_format`, `safety_compliance`, `robustness`, `tone_consistency`, `harness_integration`. Invalid keys produce a WARN and are ignored. Weights are auto-normalized to sum to 1.0. + +- **`use_case_context`**: A string describing the intended use case (e.g., "customer support bot", "code reviewer"). Helps tailor `harness_recommendations`. +- **`harness_capabilities`**: A string describing the target harness features (e.g., "tool calling, memory, guardrails"). Used to generate specific adaptation advice. + +--- diff --git a/.skills/commit-push/SKILL.md b/.skills/commit-push/SKILL.md new file mode 100644 index 000000000..f31fab939 --- /dev/null +++ b/.skills/commit-push/SKILL.md @@ -0,0 +1,415 @@ +--- +name: commit-push +description: Automates the complete git workflow: scans project rules for conventions, stages all changes, commits, pushes to the remote, and opens a Pull Request. Ensures strict compliance with documentation rules, including rule 5.4. +license: BSD 3-Clause +compatibility: Requires git CLI configured with remote access and PR creation tools (e.g., gh). +metadata: + agent: coding +--- + +# Commit & Push + +> **⚠️ EXECUTION RULE:** Every code block in this skill is a shell command to **execute**. Do not print them as text, explain them, or treat them as examples — run them directly. + +You are the final step in the craft. Precision matters. Follow these steps in order. + +## 0.5: Capture Issue ID + +If the invoking skill provides an issue ID (e.g., from `fix-issue` or `create-feature` chain), read it from the conversation history so the PR body can include a `Closes #` line. + +Look for `ISSUE_ID=` printed as structured output by the invoking skill in the conversation history. If found, set `ISSUE_ID` to that value. + +If `ISSUE_ID` is set, the PR body will include `Closes #` so the source issue auto-closes when the PR merges. + +--- + +## 1. Read the Rules + +Before touching a single file, scan the project root and any specified paths for an `AGENTS.md` rules file and read its contents. + +- Identify the required commit message format (e.g., Conventional Commits). +- Identify the required PR title/body format (look for patterns like a PR template reference). +- **Crucially:** Locate and strictly follow **Rule 5.4** (Pull Request Templates section). Extract the rules about how PR title and body should be constructed. +- **If Rule 5.4 is not found:** Log a WARN and proceed without it — do not block on missing documentation. + +## 2. Check for Detached HEAD + +Before doing anything else, verify git is not in a detached HEAD state: + +```bash +git branch --show-current +``` + +If the output is empty (detached HEAD), create a branch from the current commit: + +```bash +git checkout -b "detached-fix-$(date -u +%s)" +``` + +## 3. Create & Checkout a Branch + +First, determine the repo dynamically (needed for later PR creation): + +```bash +GIT_REMOTE=$(git remote get-url origin 2>/dev/null) +if echo "$GIT_REMOTE" | grep -q '^git@'; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\).git$/\1/') +elif echo "$GIT_REMOTE" | grep -q 'github\.com'; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\).git$/\1/') +fi + +if [ -z "$GH_REPO" ]; then + echo "ERROR: Could not determine repository from git remote '$GIT_REMOTE'." + exit 1 +fi + +# Derive the default assignee from the repo owner +GH_OWNER=$(echo "$GH_REPO" | cut -d/ -f1) +echo "GH_REPO=$GH_REPO" +echo "GH_OWNER=$GH_OWNER" +``` + +Check if already on a feature branch. If the current branch matches `feat/*`, `fix/*`, `docs/*`, or `chore/*`, skip branch creation and proceed to Step 4: + +```bash +CURRENT_BRANCH=$(git branch --show-current) +if echo "$CURRENT_BRANCH" | grep -qE '^(feat|fix|docs|chore)/'; then + echo "Already on feature branch: $CURRENT_BRANCH. Skipping branch creation." +else + SYNTHESIZE_BRANCH=true +fi +``` + +If branch creation is needed (`SYNTHESIZE_BRANCH=true`), synthesize the branch name from the **currently staged files** (or unstaged if none are staged yet): + +```bash +# Show staged files +CHANGED_FILES=$(git diff --cached --name-only 2>/dev/null || true) + +# If nothing is staged, show all changes (staged + unstaged) +if [ -z "$CHANGED_FILES" ]; then + CHANGED_FILES=$(git diff --name-only 2>/dev/null || true) +fi +``` + +**Branch name strategy** (in priority order): + +1. If `CHANGED_FILES` is empty (no changes), use a generic slug: `feat/no-changes`. Log a warning and proceed to staging. +2. If any file matches `src//` or `tests//`, extract `` as the slug. +3. If no module pattern matches, take the first changed file, extract basename, strip directories/extensions/special characters to form the slug. +4. Prepend the appropriate prefix based on the commit type (inferred in Step 1 from the scanned rules, or default to `feat`). + +```bash +# Infer COMMIT_TYPE from the rules scanned in Step 1, or default to feat. +COMMIT_TYPE="feat" + +# Determine SLUG from CHANGED_FILES +SLUG="no-changes" +if [ -n "$CHANGED_FILES" ]; then + # Priority 2: Check for src// or tests// pattern + SLUG=$(echo "$CHANGED_FILES" | grep -oE '(src|tests)/[^/]+' | sed 's/.*\///' | head -1) + + # Priority 3: Fall back to first file basename + if [ -z "$SLUG" ]; then + FIRST_FILE=$(echo "$CHANGED_FILES" | head -1) + SLUG=$(basename "$FIRST_FILE" | sed 's/\.[^.]*$//' | sed 's/[^a-zA-Z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//') + fi + + # Default to first file if still empty + if [ -z "$SLUG" ]; then + SLUG="changes" + fi +fi + +git checkout -b "${COMMIT_TYPE}/${SLUG}" +echo "BRANCH_NAME=${COMMIT_TYPE}/${SLUG}" +``` + +## 4. Stage Everything + +```bash +git add -A +``` + +Check the status to ensure nothing unexpected is included: + +```bash +git status +``` + +If there are untracked files that shouldn't be committed, exclude them via `.gitignore` and log a warning before proceeding. + +**Check for no changes:** If `git status --porcelain` returns nothing after staging, there are no changes to commit. Report this and stop — do not create an empty commit. + +## 5. Commit + +Craft the commit message based on the commit message format identified in Step 1. + +### 5.1 Determining the commit type + +- If the rules specify Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, etc.), use the prefix that best matches the bulk of changes. +- For documentation-only changes: `docs:` +- For bug fixes: `fix:` +- For new skills/features: `feat:` +- For maintenance: `chore:` + +**Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to pick the commit type instead of reasoning it out manually. Pass the staged changes as state. + +``` +Question: "What commit type best matches the bulk of these changes?" +Type: choice +Options: feat, fix, docs, chore, refactor, test +Criteria: + feat = new skills/features + fix = bug fixes + docs = documentation-only changes + chore = maintenance + refactor = refactoring with no behavior change + test = test-only changes +Example answer: fix +``` + +Interpret the returned label directly. Fall back to the rules above if no decision tool is available. + +### 5.2 Crafting the message + +Keep the subject line under 72 characters. Summarize the change in imperative mood. + +```bash +git commit -m ": " +``` + +Example: +```bash +git commit -m "docs: correct branch naming in AGENTS.md" +``` + +If the changes span multiple categories, choose the most impactful one and add a body line for additional clarity: + +```bash +git commit -m ": + +Additional context if needed." +``` + +## 6. Push + +Push the branch to the remote — this is the point of the skill. + +```bash +git push origin HEAD +``` + +**Handle push failures:** + +| Failure | Action | +|---------|--------| +| Remote not configured | Report error and suggest adding a remote (`git remote add origin `) | +| Permission denied | Report error and suggest checking SSH keys or token permissions | +| Merge conflicts | Report error and suggest rebasing (`git rebase origin/main`) before pushing | +| Other errors | Report error and stop. Do not proceed to PR creation | + +## 7. Check for Existing PR + +Before creating a new PR, check whether one already exists: + +```bash +EXISTING_PR_URL=$(gh pr list --head "$(git branch --show-current)" --base main --state open --json url --jq '.[0].url' 2>/dev/null || true) +``` + +- **If a URL is returned:** Do **not** create a new PR. Extract the PR number and print it as structured output: + ``` + PR_NUMBER= + PR_URL= + ``` + Then skip to Step 9. + +- **If no URL is returned:** Proceed to Step 8. + +## 8. Open a Pull Request + +**Verify `gh` CLI is authenticated:** + +```bash +gh auth status 2>&1 +``` + +If authenticated, continue. If not, report `gh` authentication failure and instruct the user to run `gh auth login`. + +### 8.1 Synthesize PR Title + +- Follow the commit message format from Step 1 if specific rules exist. +- Use the same type prefix and summary as the commit message. + +Example: `docs: correct branch naming in AGENTS.md` + +### 8.2 Synthesize PR Body + +Pull the PR body from the project's template. The template path is defined by Rule 5.4. The standard location is `.github/PULL_REQUEST_TEMPLATE.md` — try it first. If it does not exist, check `.github/gh-pull_request_template.md`, then `.github/PULL_REQUEST_TEMPLATE.md` in other common variants. If none exist, generate a minimal body from the available context. + +```bash +# Try common template locations in order +TEMPLATE_PATHS=( + ".github/PULL_REQUEST_TEMPLATE.md" + ".github/gh-pull_request_template.md" +) + +TEMPLATE_FILE="" +for path in "${TEMPLATE_PATHS[@]}"; do + if [ -f "$path" ]; then + TEMPLATE_FILE="$path" + break + fi +done +``` + +**If a template file was found** (`TEMPLATE_FILE` is set), read it as the base body and fill in each section with the actual content: + +- Replace inline placeholders (e.g., ``, ``, `[ ]` checkboxes). +- If the template has no fillable placeholders, append the content as a "Details" section at the end. + +**Write the filled-in body to a temp file** — do NOT pass the template file path as the body: + +**Cleanup:** Remove `BODY_FILE` once the PR is created, or when cleaning up after a failure. Do not rely on a shell `trap` — delete the temp file explicitly when the task completes. + +```bash +BODY_FILE=$(mktemp) + +# Capture the commit subject and changed files for substitution +COMMIT_SUBJECT=$(git log -1 --oneline | sed 's/^[a-f0-9]* //') +# Handle root commit (no parent) gracefully +if git rev-parse HEAD~1 >/dev/null 2>&1; then + CHANGED_FILES=$(git diff --name-only HEAD~1 | head -5 | tr '\n' ', ' | sed 's/,$//') +else + CHANGED_FILES=$(git diff --name-only --cached | head -5 | tr '\n' ', ' | sed 's/,$//') +fi + +# Read the template, replace placeholders with actual content, write to temp file +sed -e "s//$COMMIT_SUBJECT/g" \ + -e "s//$CHANGED_FILES/g" \ + "$TEMPLATE_FILE" > "$BODY_FILE" +``` + +**If no template file exists**, generate a minimal body and write it to a temp file: + +```bash +BODY_FILE=$(mktemp) + +# Handle root commit (no parent) gracefully +if git rev-parse HEAD~1 >/dev/null 2>&1; then + CHANGED_FILES=$(git diff --name-only HEAD~1 | head -5 | tr '\n' ', ' | sed 's/,$//') +else + CHANGED_FILES=$(git diff --name-only --cached | head -5 | tr '\n' ', ' | sed 's/,$//') +fi + +cat > "$BODY_FILE" << BODYEOF +**Commit:** \`$(git log -1 --oneline)\` + +**Changed files:** \`$CHANGED_FILES\` +BODYEOF +``` + +**If `ISSUE_ID` is set**, append a `Closes` line to the body file so the source issue auto-closes when the PR merges: + +```bash +if [ -n "$ISSUE_ID" ]; then + echo "" >> "$BODY_FILE" + echo "Closes #$ISSUE_ID" >> "$BODY_FILE" +fi +``` + +### 8.3 Create the PR + +**If `TEMPLATE_FILE` was found in Step 8.2:** Use the filled-in body from the temp file (Rule 5.4 compliance). Fill in every section from the template — do not leave any blank. If a section is not applicable, write `N/A`. + +```bash +# Use the populated body file, not the raw template +# Include --repo for worktree compatibility +gh pr create \ + --title "" \ + --body-file "$BODY_FILE" \ + --base main \ + --assignee "$GH_OWNER" \ + --repo "$GH_REPO" +``` + +If `gh pr create` does not support `--body-file` in your version, use stdin: + +```bash +gh pr create \ + --title "" \ + --body "@-" \ + --base main \ + --assignee "$GH_OWNER" \ + --repo "$GH_REPO" < "$BODY_FILE" +``` + +**If no template file exists:** Use the minimal body from the temp file: + +```bash +gh pr create \ + --title "" \ + --body-file "$BODY_FILE" \ + --base main \ + --assignee "$GH_OWNER" \ + --repo "$GH_REPO" +``` + +**Label selection:** Infer from the commit type determined in Step 5.1: +- If the subject starts with or implies a bug fix → add `--label "bug"` +- Otherwise → add `--label "feature"` +- If multiple types are present, use `"bug"` only if any commit is a fix; otherwise `"feature"` + +**After creating the PR, print the structured output:** + +``` +PR_NUMBER= +PR_URL= +``` + +Extract the number from the `gh pr create` output or from the PR URL. The URL format is `https://github.com///pull/`. + +## 9. Verification + +### 9.1 Verify PR Body + +After the PR is created (Step 8) or updated (Step 7), verify the PR body was set correctly by reading it back from the API. Use the `PR_NUMBER` from the structured output in Step 7 or 8: + +```bash +ACTUAL_BODY=$(gh pr view "$PR_NUMBER" --json body --jq '.body' 2>/dev/null || true) +EXPECTED_BODY=$(cat "$BODY_FILE" 2>/dev/null || true) +if [ -n "$ACTUAL_BODY" ] && [ -n "$EXPECTED_BODY" ]; then + if [ "$ACTUAL_BODY" = "$EXPECTED_BODY" ]; then + echo "PR body verified successfully." + else + echo "WARNING: PR body mismatch detected." + echo "Expected:" + echo "$EXPECTED_BODY" + echo "" + echo "Actual:" + echo "$ACTUAL_BODY" + echo "" + echo "Attempting to fix..." + gh api "repos/$GH_REPO/pulls/$PR_NUMBER" -X PATCH -f body="$EXPECTED_BODY" 2>/dev/null || true + fi +fi +``` + +### 9.2 Confirm PR URL + +- Confirm the PR was created successfully (or note the existing PR URL from Step 7). +- Output the PR URL for the user so they can review it. + +Example output: +``` +PR created: https://github.com///pull/ +``` + +## Gotchas + +- **Never commit directly to `main`.** Always create a feature branch first. The skill synthesizes a branch name automatically if none exists. +- **Empty change sets are silently rejected.** If `git status --porcelain` returns nothing after staging, the skill stops — no empty commits are created. +- **Existing PRs are detected and reused.** If a PR already exists for the branch, the skill skips PR creation and reports the existing PR number. Do not create duplicate PRs. +- **PR template sections must never be left blank.** If a section does not apply, write `N/A` rather than skipping it. + +## Error Handling \ No newline at end of file diff --git a/.skills/create-feature/SKILL.md b/.skills/create-feature/SKILL.md new file mode 100644 index 000000000..20ed7295b --- /dev/null +++ b/.skills/create-feature/SKILL.md @@ -0,0 +1,672 @@ +--- +name: create-feature +description: Orchestrates a complete feature lifecycle: receive goals, synthesize specs, propose via OpenSpec, commit specs to PR, apply tasks, commit implementation, archive, update PR, and post audit results. Creates an isolated git worktree for each run. +license: BSD 3-Clause +compatibility: Requires Node.js 24+, npm, git with remote access, gh CLI, openspec CLI, and a project root with openspec/ directory. Requires git worktree support. When invoked as a sub-agent, requires a 30–60 minute timeout — the full pipeline (spec → PR → implement → archive → update PR) can take that long. +metadata: + agent: coding +--- + +# Create Feature + +> **⚠️ EXECUTION RULE:** Every code block in this skill is a shell command to **execute**. Do not print them as text, explain them, or treat them as examples — run them directly. + +Orchestrate a complete feature lifecycle from raw goals to shipped code. This is the full pipeline — synthesis, specification, implementation, verification, delivery, and archive. + +## Prerequisites + +Before starting, ensure: +- You are in the project root directory (contains `package.json`, `openspec/`) +- `gh` CLI is authenticated and configured +- `openspec` CLI is available +- The current branch is `main` (or will be checked out at step 0) + +## Input Parsing + +The input may include a `BRANCH_TYPE` prefix (e.g., `BRANCH_TYPE=fix create-feature ...`). Extract it if present in the conversation context. + +Read `BRANCH_TYPE` from the conversation history if provided by the invoking skill. If not present, default to `feat`. + +## Input + +The user provides a list of goals/features in any format (natural language, JSON, markdown list, etc.). Parse and normalize them into a clean array of goal strings. + +**Example input:** +> Add a new tool that can summarize web pages, and improve the TUI memory panel to show retention stats. + +**Normalized goals:** +1. Add a new tool that can summarize web pages +2. Improve the TUI memory panel to show retention stats + +--- + +## Step 0: Ensure Clean State + +**Capture the project root and repository info (needed for worktree paths and gh commands):** + +```bash +PROJECT_ROOT=$(git rev-parse --show-toplevel) +GIT_REMOTE=$(git remote get-url origin 2>/dev/null) +if echo "$GIT_REMOTE" | grep -q '^git@'; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\).git$/\1/') +elif echo "$GIT_REMOTE" | grep -q 'github\.com'; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\).git$/\1/') +fi + +if [ -z "$GH_REPO" ]; then + echo "ERROR: Could not determine repository from git remote '$GIT_REMOTE'." + exit 1 +fi +echo "GH_REPO=$GH_REPO" +``` + +**Create a dedicated worktree directory as a sibling of the repo root.** Placing it *outside* the repo (e.g., `madz.worktrees/` next to `madz/`) avoids nesting a git repository inside the main working tree. A nested worktree is a known git footgun: it shows up as a nested repo to `git status`/`git clean`, and requires a `.gitignore` entry to hide it. A sibling directory keeps the repo pristine — no ignore entry needed, `git clean -fdx` is a no-op, and the worktree is still tracked normally via the `.git` file pointing back into the repo's `.git/worktrees/`. + +```bash +WORKTREES_DIR="$(dirname "$PROJECT_ROOT")/$(basename "$PROJECT_ROOT").worktrees" +mkdir -p "$WORKTREES_DIR" +``` + +```bash +# Only checkout main if not already on it +if [ "$(git branch --show-current)" != "main" ]; then + # Verify main exists on remote before pulling + REMOTE_MAIN=$(git ls-remote --heads origin main 2>/dev/null) + if [ -n "$REMOTE_MAIN" ]; then + git fetch origin main + git checkout main + git pull origin main + else + echo "WARNING: No remote branch 'main' found. Current branch '$(git branch --show-current)' will be used as-is." + fi +fi +``` + +Verify the working tree is clean: +```bash +git status --porcelain +``` + +If there are uncommitted changes, report them and stop. Do not proceed with a dirty tree. + +--- + +## Step 0.5: Capture Session ID + +Capture a unique session identifier so that multiple instances (e.g., subagents) can run in parallel without file collisions. + +```bash +# Portable: works in Alpine, minimal images, and standard Linux +# Generates 8 random hex chars from /dev/urandom, falls back to $RANDOM if unavailable +SESSION_ID=$(head -c 8 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' || echo $RANDOM$RANDOM$RANDOM) +echo "SESSION_ID=$SESSION_ID" +``` + +**Where to store temp files:** The agent running this skill decides where to place temp files. Use example names like `${SESSION_ID}-feature-goals.md`, `${SESSION_ID}-feature-prompt.md`, `${SESSION_ID}-audit-results.md`, and `${SESSION_ID}-pr-number.txt`. The agent may place them in `tmp/`, `state/`, or any other directory that fits the project's conventions. + +**Cleanup:** The agent must remove all temp files and the worktree when the task completes, and also when cleaning up after a failure or interruption. Do not rely on a shell `trap` — explicitly clean up work files at the end of the task or during failure cleanup. The worktree path is defined in Step 0.75; the agent should track it and remove it as part of cleanup. + +--- + +## Step 0.75: Create Isolated Worktree + +Create a dedicated git worktree so all feature work is isolated from the main working tree. Using absolute paths (`PROJECT_ROOT`) prevents the worktree from being nested inside another worktree. + +```bash +WORKTREE_PATH="${WORKTREES_DIR}/${SESSION_ID}" + +# Create the worktree from main (--detach since main is already checked out) +git worktree add --detach "$WORKTREE_PATH" main + +# Verify worktree creation succeeded +if [ ! -d "$WORKTREE_PATH" ]; then + echo "ERROR: Worktree creation failed at $WORKTREE_PATH." + exit 1 +fi + +# Record the original directory so we can return for cleanup +ORIGINAL_DIR=$(pwd) +echo "WORKTREE_PATH=$WORKTREE_PATH" +``` + +**Change into the worktree** — all subsequent steps run from here: + +```bash +cd "$WORKTREE_PATH" +``` + +**Cleanup:** When the task completes, or when cleaning up after a failure or interruption, the agent must remove the worktree and all temp files. Do not rely on a shell `trap` — explicitly clean up work files at the end of the task or during failure cleanup. Remove the worktree with `git worktree remove "$WORKTREE_PATH" --force` (falling back to `rm -rf "$WORKTREE_PATH"` if needed), return to `$ORIGINAL_DIR`, and delete the temp files (`${SESSION_ID}-feature-goals.md`, `${SESSION_ID}-feature-prompt.md`, `${SESSION_ID}-audit-results.md`, `${SESSION_ID}-pr-number.txt`, `${SESSION_ID}-test-patterns.md`). + +--- + +## Step 1: Synthesize Detailed Goals + +Take the raw goals from the input and expand each into a more detailed, actionable specification. For each goal, produce: + +- **Goal:** The original goal string +- **Scope:** What is included and explicitly excluded +- **Key Requirements:** 3-5 concrete requirements +- **Acceptance Criteria:** How success is measured +- **Dependencies:** Any existing code, configs, or specs that need to change +- **Risks / Edge Cases:** Potential pitfalls to consider + +Write the detailed goals to a file named `${SESSION_ID}-feature-goals.md` (use an example name — the agent decides where to place it). This file serves as the source of truth for all subsequent audits. + +--- + +## Step 2: Synthesize Proposal Prompt + +Using the detailed goals from Step 1, synthesize a multi-paragraph proposal prompt suitable for feeding into `openspec-propose`. The prompt should: + +- Start with a parseable change name on the first line: `CHANGE_NAME: ` (e.g., `CHANGE_NAME: web-page-summarizer-tui-memory-stats`) +- Include a concise summary of what is being built and why +- Describe the technical approach in 2-4 paragraphs +- Reference specific files, modules, and patterns in the codebase +- Note any architectural decisions or trade-offs +- Be specific enough that `openspec new change` + artifact generation will produce useful specs + +Write the proposal prompt to a file named `${SESSION_ID}-feature-prompt.md` (use an example name — the agent decides where to place it). + +--- + +## Step 3: Extract Change Name + +Parse the change name from the proposal prompt in Step 2. This is the kebab-case identifier that will be used for the OpenSpec change directory. + +```bash +CHANGE_NAME=$(grep -o 'CHANGE_NAME: .*' "${SESSION_ID}-feature-prompt.md" | sed 's/^CHANGE_NAME: //' | sed 's/ *$//' | head -1) +if [ -z "$CHANGE_NAME" ]; then + CHANGE_NAME=$(basename "$(pwd)" | tr '[:upper:]' '[:lower:]' | tr ' ' '-') +fi +echo "CHANGE_NAME=$CHANGE_NAME" +``` + +--- + +## Step 3.5: Create Feature Branch + +Determine the branch type from the input context. If the input contains a reference to an issue with a known type (e.g., from `fix-issue` chain), extract it. Otherwise, default to `feat`. + +Valid types: `feat`, `fix`, `chore`, `docs`, `test`. + +Strip any existing type prefix from `$CHANGE_NAME` to avoid double-prefixing: + +```bash +# Determine branch type — default to feat if not specified +BRANCH_TYPE="${BRANCH_TYPE:-feat}" + +# Validate branch type +case "$BRANCH_TYPE" in + feat|fix|chore|docs|test) ;; + *) BRANCH_TYPE="feat" ;; +esac + +# Strip any existing type prefix from CHANGE_NAME to avoid double-prefixing +BRANCH_NAME=$(echo "$CHANGE_NAME" | sed 's/^\(feat\|fix\|chore\|docs\|test\)\///') +git checkout -b "${BRANCH_TYPE}/${BRANCH_NAME}" +``` + +Verify the branch was created: +```bash +git branch --show-current +``` + +--- + +## Step 4: Create OpenSpec Change + +Run the OpenSpec propose workflow for the synthesized change: + +```bash +openspec new change "$CHANGE_NAME" +``` + +Then generate all artifacts using this procedure (optimized for the automated pipeline): + +1. Get the artifact build order: + ```bash + openspec status --change "$CHANGE_NAME" --json + ``` + +2. For each artifact that is `ready` (dependencies satisfied): + - Get instructions: `openspec instructions --change "$CHANGE_NAME" --json` + - Read any completed dependency files for context + - Create the artifact file using the template and following the instructions + - Re-run `openspec status --change "$CHANGE_NAME" --json` after each artifact + +3. Continue until all `applyRequires` artifacts are complete. + +After completion, verify all expected files exist: +```bash +ls -la openspec/changes/$CHANGE_NAME/ +``` + +Expected files: `.openspec.yaml`, `proposal.md`, `design.md`, `tasks.md`, and any spec deltas in `specs/`. + +If any expected file is missing, stop and report the error. + +--- + +## Step 5: Audit Specs Against Goals (Up to 3 Iterations) + +Read the detailed goals from the file named `${SESSION_ID}-feature-goals.md` (use an example name — the agent decides where to place it) and the generated spec documents (proposal.md, design.md, tasks.md, and any spec deltas). + +Perform a thorough audit: + +1. **Coverage audit:** Does every detailed goal have corresponding requirements in the specs? +2. **Fidelity audit:** Do the specs faithfully represent the original intent of each goal? +3. **Completeness audit:** Are there missing requirements, edge cases, or acceptance criteria not captured? +4. **Consistency audit:** Do the tasks in tasks.md map to the requirements in the specs? + +Write audit findings to a file named `${SESSION_ID}-audit-results.md` (use an example name — the agent decides where to place it). + +**Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to decide whether errors remain before iterating. Pass the audit findings as state. + +``` +Question: "Do the specs faithfully cover all goals with no errors?" +Type: noul +Criteria: + false = missing requirements, fidelity gaps, or task-spec mismatch + true = all goals covered, faithful, complete, consistent +Example answer: false +``` + +**Important:** The `noul` type returns a *single float in [0,1]*, not a label — it is the probability of `true`. Interpret it as: `≥ 0.6` → true (no errors, proceed), `≤ 0.4` → false (errors remain, iterate), between → unknown. Fall back to manual review if no decision tool is available. + +**If errors are found:** +- Fix the spec documents to address each finding +- Update tasks.md if task-to-spec mapping is broken +- Re-audit (increment iteration count) +- Repeat until no errors remain or 3 iterations are exhausted + +**If no errors are found:** +- Proceed to Step 6 + +--- + +## Step 6: Commit & Push OpenSpec Files (via commit-push) + +**Purpose:** Lock the spec documents into a pull request before implementation begins. + +**Do not perform git operations inline.** Invoke the `commit-push` skill to handle staging, committing, pushing, and PR creation. + +The `commit-push` skill will: +- Stage all OpenSpec files and spec deltas +- Commit using conventional commit format from the scanned project rules §5.1 +- Push to the remote +- Create a PR using the template from the scanned project rules §5.4 +- Print structured output: `PR_NUMBER=` and `PR_URL=` + +Read the `PR_NUMBER` from the structured output printed by `commit-push`. Do not attempt to grep a shell variable — the value is in the conversation history. + +**Capture `PR_NUMBER` from the conversation history** (the chained skill prints `PR_NUMBER=` as structured output). Read it from the conversation context, not from a shell variable: + +```bash +# PR_NUMBER is captured from the conversation history, not a shell variable. +# The chained skill prints lines like: PR_NUMBER=42 or PR_URL=https://... +# Use grep to find lines matching the pattern, take the last one +PR_NUMBER=$(echo "$CONVERSATION_HISTORY" | grep -oE '^PR_NUMBER=[0-9]+' | tail -1 | cut -d= -f2) +if [ -z "$PR_NUMBER" ]; then + echo "ERROR: Could not capture PR_NUMBER from commit-push output. Conversation history:" + echo "$CONVERSATION_HISTORY" | tail -20 + exit 1 +fi +echo "PR_NUMBER=$PR_NUMBER" +``` + +**Note:** `$CONVERSATION_HISTORY` is not a real environment variable. The agent running this skill substitutes the actual conversation text when executing the command. If the conversation history is not available as a shell variable, the agent should read the PR number directly from the conversation context instead of running this grep command. + +Write it to a file for later use in Step 12: + +```bash +echo "" > "${SESSION_ID}-pr-number.txt" +``` + +Replace `` with the actual number read from the conversation history. + +If `commit-push` fails, report the error and stop. Do not attempt to recover with manual git commands. + +**After reading the PR number, continue to Step 7.** Do not stop or wait for further input — the pipeline proceeds automatically. + +--- + +## Step 6.5: Discover Related Tests + +Before implementing, read existing tests to understand the project's testing conventions, patterns, and structure. This ensures new tests (if any are required by the tasks) follow the established style. + +**Identify source files from the spec:** Look at the tasks in `tasks.md`, the design document, and spec deltas to determine which source files will be created or modified. + +**Find related tests using these rules:** + +1. **Nested test directory** (most common): If the source file lives in `src/foo/bar.js`, look for `tests/unit/foo/bar.test.js` or `tests/unit/bar.test.test.js`. Mirror the directory structure. +2. **Flat test directory**: If tests live in a flat `tests/` or `tests/unit/` directory with no nesting, find tests by filename matching — e.g., `src/foo/bar.js` → `tests/bar.test.js` or `tests/bar.test.test.js`. +3. **Sibling test file**: Some projects place test files alongside source — e.g., `src/foo/bar.js` with `src/foo/bar.test.js` or `src/foo/bar.spec.js`. +4. **Module-level test**: If no exact match exists, look for a test file that covers the parent module or directory — e.g., `tests/unit/foo.test.js` covering everything in `src/foo/`. + +**For each source file being created or modified:** + +```bash +# Example: find related tests for src/scheduler/runner.js +# 1. Check nested test directory +ls tests/unit/scheduler/runner.test.js 2>/dev/null && echo "found: tests/unit/scheduler/runner.test.js" + +# 2. Check flat test directory by name +ls tests/runner.test.js 2>/dev/null && echo "found: tests/runner.test.js" + +# 3. Check sibling test file +ls src/scheduler/runner.test.js 2>/dev/null && echo "found: src/scheduler/runner.test.js" + +# 4. Check parent module test +ls tests/unit/scheduler.test.js 2>/dev/null && echo "found: tests/unit/scheduler.test.js" +``` + +Read every test file you discover. Pay attention to: +- **Assertion style** — `assert`, `chai`, `node --test`, custom helpers +- **Mocking patterns** — how dependencies, external services, and config are mocked +- **Test structure** — `describe`/`it` nesting, fixture setup, teardown +- **Naming conventions** — test file and test case naming +- **Common helpers** — shared test utilities, factories, or fixtures + +Write a brief summary of the patterns you found to `${SESSION_ID}-test-patterns.md` (use an example name — the agent decides where to place it). This becomes context for Step 7 when implementing tests. + +If no related tests exist, note "No existing tests found — follow project conventions from AGENTS.md §3.5" and proceed. + +--- + +## Step 7: Apply Tasks (via openspec-apply-change) + +Invoke the `openspec-apply-change` skill to implement all tasks from `tasks.md`. + +The `openspec-apply-change` skill will: +- Read the tasks from `tasks.md` +- Implement each task sequentially +- Mark tasks complete as `[x]` in `tasks.md` +- Report progress as "N/M tasks complete" + +**Wait for the skill to complete.** Do not proceed to Step 7.5 until all tasks are marked complete. + +If the skill reports that tasks are blocked (missing artifacts), report the error and stop. Do not proceed with incomplete work. + +**If interrupted (timeout, context limit):** Re-invoke `openspec-apply-change "$CHANGE_NAME"` to resume from where it left off. The skill checks task status and only implements remaining tasks. + +After completion, verify: +- All tasks in `tasks.md` are marked `[x]` + +**After verification, continue to Step 7.5.** Do not stop or wait for further input — the pipeline proceeds automatically. + +**Check which npm scripts are available before running them** — not all projects define the same scripts: + +```bash +AVAILABLE_SCRIPTS=$(node -e "console.log(Object.keys(require('./package.json').scripts || {}).join('\n'))" 2>/dev/null) +``` + +- If `test` script exists, run: `npm run test` +- If `lint` script exists, run: `npm run lint` +- If `coverage` script exists, run: `npm run coverage` +- If no script exists in package.json, skip the step and note "No script defined — skipped." + +If any verification fails, fix the issues and re-verify. + +--- + +## Step 7.5: Commit & Push Implementation Code (via commit-push) + +**Purpose:** Push the code produced by `openspec-apply-change` to the open PR. + +Invoke the `commit-push` skill to stage, commit, push, and update the existing PR. + +This will: +- Stage all implementation files (not openspec files — they are already committed) +- Commit using conventional commit format from the scanned project rules §5.1 +- Push to the remote +- Update the existing PR (created in Step 6) if `commit-push` detects a pre-existing PR +- Print structured output: `PR_NUMBER=` and `PR_URL=` + +Read the `PR_NUMBER` from the structured output. If `commit-push` fails, report the error and stop. Do not attempt to recover with manual git commands. + +**Capture `PR_NUMBER` from the conversation history** (the chained skill prints `PR_NUMBER=` as structured output). Read it from the conversation context, not from a shell variable: + +```bash +# PR_NUMBER is captured from the conversation history, not a shell variable. +PR_NUMBER=$(echo "$CONVERSATION_HISTORY" | grep -oE '^PR_NUMBER=[0-9]+' | tail -1 | cut -d= -f2) +if [ -z "$PR_NUMBER" ]; then + echo "ERROR: Could not capture PR_NUMBER from commit-push output." + exit 1 +fi +echo "PR_NUMBER=$PR_NUMBER" +``` + +**Note:** `$CONVERSATION_HISTORY` is not a real environment variable. The agent substitutes the actual conversation text when executing the command. If the conversation history is not available as a shell variable, read the PR number directly from the conversation context instead. + +**After `/commit-push` completes, continue to Step 8.** Do not stop or wait for further input — the pipeline proceeds automatically. + +--- + +## Step 8: Verify Application Starts (`npm start`) + +After tasks are applied, verify the application actually starts without crashing. + +**Run with a timeout** — use a portable approach that works on both GNU and BSD: + +```bash +# Portable timeout: use Perl if available, otherwise background + sleep + kill +if command -v timeout >/dev/null 2>&1; then + timeout 10 npm start 2>&1 || true +else + npm start & + NPM_PID=$! + sleep 10 + kill $NPM_PID 2>/dev/null || true + wait $NPM_PID 2>/dev/null || true +fi +``` + +If the application fails to start, fix the issue before proceeding. Do not skip this step — a crashing application means the implementation is flawed. + +--- + +## Step 9: Audit Results Against Specs & Goals (Up to 3 Iterations) + +Read the original goals from the file named `${SESSION_ID}-feature-goals.md` (use an example name — the agent decides where to place it) and the spec documents. Audit the implemented results: + +1. **Goal fulfillment:** Does the implementation satisfy every detailed goal? +2. **Spec compliance:** Does the code match the requirements in the spec documents? +3. **Task completion:** Were all tasks in tasks.md actually implemented correctly? +4. **Quality check:** Are there any obvious issues, missing edge cases, or inconsistencies? + +Write audit findings to the file named `${SESSION_ID}-audit-results.md` (use an example name — the agent decides where to place it) (overwrite previous results). + +**Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to decide whether errors remain before iterating. Pass the audit findings as state. + +``` +Question: "Does the implementation satisfy every goal with no errors?" +Type: noul +Criteria: + false = missing goals, spec violations, incomplete tasks, or quality issues + true = all goals fulfilled, spec-compliant, tasks complete, no obvious issues +Example answer: false +``` + +**Important:** The `noul` type returns a *single float in [0,1]*, not a label — it is the probability of `true`. Interpret it as: `≥ 0.6` → true (no errors, proceed), `≤ 0.4` → false (errors remain, iterate), between → unknown. Fall back to manual review if no decision tool is available. + +**If errors are found:** +- Fix the code to address each finding +- Re-verify tests and lint pass +- Re-audit (increment iteration count) +- Repeat until no errors remain or 3 iterations are exhausted + +**If no errors are found:** +- Proceed to Step 10 + +--- + +## Step 10: Archive and Push (openspec-archive-change → commit-push) + +**Purpose:** Archive the OpenSpec change and push the archive (and any remaining implementation fixes from Step 9) to the open PR. + +1. **Archive the OpenSpec change with automatic sync:** + + 1. **Archive with retry on validation failure** (up to 3 attempts): + + ```bash + ARCHIVE_ATTEMPTS=0 + ARCHIVE_MAX=3 + while [ $ARCHIVE_ATTEMPTS -lt $ARCHIVE_MAX ]; do + ARCHIVE_ATTEMPTS=$((ARCHIVE_ATTEMPTS + 1)) + echo "Archive attempt $ARCHIVE_ATTEMPTS/$ARCHIVE_MAX..." + + ARCHIVE_OUTPUT=$(openspec archive "$CHANGE_NAME" --yes 2>&1) + ARCHIVE_EXIT=$? + + if [ $ARCHIVE_EXIT -eq 0 ]; then + echo "$ARCHIVE_OUTPUT" > "${SESSION_ID}-archive-output.md" + echo "Archive succeeded on attempt $ARCHIVE_ATTEMPTS." + break + fi + + echo "Archive failed (attempt $ARCHIVE_ATTEMPTS): $ARCHIVE_OUTPUT" + echo "$ARCHIVE_OUTPUT" > "${SESSION_ID}-archive-error.md" + + if [ $ARCHIVE_ATTEMPTS -ge $ARCHIVE_MAX ]; then + echo "ERROR: Archive failed after $ARCHIVE_MAX attempts. Stopping." + exit 1 + fi + done + ``` + + 2. **When presented with a sync prompt**, automatically choose **sync** (the recommended option). Do not require user input — just select sync: + - If the prompt shows "Sync now (recommended)" / "Archive without syncing" → choose "Sync now (recommended)" + - If the prompt shows "Archive now" / "Sync anyway" / "Cancel" → choose "Archive now" + - If no delta specs exist → proceed without sync prompt + + 3. **Resolve spec issues if archive validation failed** (up to 3 fix iterations): + + If the archive step above exited with an error (i.e., `openspec archive` returned non-zero and the loop terminated via `exit 1`), the file `${SESSION_ID}-archive-error.md` contains the error details. Use those details to identify and fix the underlying spec issues: + + - Read `${SESSION_ID}-archive-error.md` to understand what validation failed (missing requirements, inconsistent specs, task-to-spec mismatches, etc.) + - Fix the relevant spec documents (proposal.md, design.md, tasks.md, and any spec deltas in `specs/`) + - Update tasks.md if task-to-spec mapping is broken + - Re-run the archive attempt (go back to step 1 above) + - Repeat until archive succeeds or 3 fix iterations are exhausted + + **If archive still fails after 3 fix iterations**, report the remaining validation errors and stop. Do not proceed with an unarchived change. + + 4. **Verify** the archive was created: + ```bash + ls openspec/changes/archive/ + ``` + + The change directory should now be under `openspec/changes/archive/YYYY-MM-DD-/`. + +2. **Push the archive (and all remaining changes) to the open PR** by invoking `commit-push`. + + This will: + - Stage the archived change directory and any delta specs synced from the archive + - Stage any remaining implementation fixes from Step 9 + - Commit using conventional commit format from the scanned project rules §5.1 + - Push to the remote + - Update the existing PR (created in Step 6) if `commit-push` detects a pre-existing PR + - Print structured output: `PR_NUMBER=` and `PR_URL=` + + Read the `PR_NUMBER` from the structured output. If `commit-push` fails, report the error and stop. Do not attempt to recover with manual git commands. + + Add a reusable capture pattern to extract `PR_NUMBER` from the conversation history after `commit-push` completes: + + ```bash + # Capture PR_NUMBER from the conversation history (last occurrence wins) + PR_NUMBER=$(echo "$CONVERSATION_HISTORY" | grep -oE '^PR_NUMBER=[0-9]+' | tail -1 | cut -d= -f2) + if [ -z "$PR_NUMBER" ]; then + echo "ERROR: Could not capture PR_NUMBER from commit-push output." + exit 1 + fi + echo "PR_NUMBER=$PR_NUMBER" + ``` + +**After the archive and push complete, continue to Step 11.** Do not stop or wait for further input — the pipeline proceeds automatically. + +--- + +## Step 11: Update PR Title & Description (via update-pr) + +**Purpose:** Set the final, accurate PR title and description reflecting what was actually implemented. + +Invoke the `update-pr` skill to update the PR. This skill scans project rules for conventions and the PR template, synthesizes both from the delta between the branch and target, and applies changes via `gh api`. + +The `update-pr` skill will: +- Gather the actual changes made (`git diff main...HEAD --stat`, `git diff main...HEAD --name-only`) +- Synthesize a final PR description that reflects what was actually implemented, references specific files and modules changed, notes any deviations from the original plan, and includes a testing coverage summary +- Follow the PR template format from the scanned project rules §5.4 +- Update the PR using `gh api` + +After `update-pr` completes, verify the PR was updated correctly by checking the PR on GitHub. + +Add a reusable capture pattern to confirm `PR_NUMBER` is still available from the conversation history after `update-pr` completes: + +```bash +# PR_NUMBER was already captured from earlier commit-push steps. +# Re-capture to confirm it's still available after update-pr. +PR_NUMBER=$(echo "$CONVERSATION_HISTORY" | grep -oE '^PR_NUMBER=[0-9]+' | tail -1 | cut -d= -f2) +if [ -z "$PR_NUMBER" ]; then + echo "ERROR: PR_NUMBER not found in conversation history after update-pr." + exit 1 +fi +echo "PR_NUMBER=$PR_NUMBER (verified after update-pr)" +``` + +**After verification, continue to Step 12.** Do not stop or wait for further input — the pipeline proceeds automatically. + +--- + +## Step 12: Post Audit Results as PR Comment + +Post the final audit results from Step 9 as a comment on the PR: + +```bash +PR_NUMBER=$(cat "${SESSION_ID}-pr-number.txt") +gh pr comment "$PR_NUMBER" --body "$(cat "${SESSION_ID}-audit-results.md")" --repo "$GH_REPO" +``` + +--- + +## Step 13: Final Report + +Print a summary: + +``` +Feature lifecycle complete. +Change: $CHANGE_NAME +PR: +Goals addressed: / +Iterations: , +Tests: passing +Lint: passing +Coverage: maintained +``` + +--- + +## Step 14: Cleanup + +Cleanup is the agent's responsibility. When the task completes, or when cleaning up after a failure or interruption, remove the worktree and all temp files as described in Step 0.75. Do not rely on a shell `trap` — explicitly clean up work files at the end of the task or during failure cleanup. + +Verify the worktree was removed: +```bash +ls "$WORKTREE_PATH" 2>&1 || echo "Worktree cleaned up successfully." +``` + +--- + +## Error Handling + +- If any step fails, report the error clearly and stop. Do not continue with incomplete work. +- If 3 audit iterations are exhausted and errors remain, report the remaining issues and stop. +- If `gh` API calls fail, report the error and provide the manual commands needed. +- Always leave the repository in a clean state (working tree clean, on the feature branch). + +## Gotchas + +- **Session IDs prevent collisions.** When running multiple instances (e.g., subagents), always use a unique `SESSION_ID` to avoid temp file collisions. +- **`commit-push` is delegated, not inline.** Steps 6, 7.5, and 10 invoke `commit-push` as a chain instruction — do not perform git operations inline at those steps. +- **The pipeline is non-blocking.** After each delegated skill completes, the pipeline proceeds automatically. Do not wait for user confirmation between steps. +- **`openspec-apply-change` is resumable.** If interrupted by timeout or context limit, re-invoke with the same `CHANGE_NAME` to resume from where it left off. +left off. diff --git a/.skills/create-issue/SKILL.md b/.skills/create-issue/SKILL.md new file mode 100644 index 000000000..c3015079d --- /dev/null +++ b/.skills/create-issue/SKILL.md @@ -0,0 +1,565 @@ +--- +name: create-issue +description: Receives a user description, synthesizes it into a title and description, categorizes as 'fix' or 'feat', creates a GitHub issue, audits the codebase for actionable details, and updates the issue with findings. +license: BSD 3-Clause +compatibility: Requires gh CLI authenticated with the repo. Must be run from the project root. +metadata: + agent: coding +--- + +# Create Issue + +> **⚠️ EXECUTION RULE:** Every code block in this skill is a shell command to **execute**. Do not print them as text, explain them, or treat them as examples — run them directly. + +Receives a user description, synthesizes it into a polished title and description, categorizes the work as a **fix** or **feat**, creates a GitHub issue, audits the codebase for actionable details, and updates the issue with findings. + +## Workflow + +### 1. Parse Input + +The user's description is provided in the command context (the text after `/create-issue`). **Use it directly.** Do not ask the user for it. + +If no description was provided, ask the user for one. + +### 2. Categorize + +**CRITICAL LABEL RULE:** The only valid GitHub labels are `bug` and `feature`. **Never** use `enhancement`, `improvement`, `refactor`, `docs`, or any other label. This is non-negotiable. + +Analyze the user's description and determine whether this is a **fix** or a **feat**: + +- **fix** — A bug, regression, unexpected behavior, crash, or something that is broken and needs repair. +- **feat** — A new capability, enhancement, improvement, or something that doesn't exist yet. + +**Default to `feat` (→ `feature` label) unless the description is clearly a bug.** If you are even slightly uncertain, it is a feature. There is no third option. + +**Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to categorize instead of reasoning it out manually. Pass the user's description as state. + +``` +Question: "Is this a fix or a feat?" +Type: choice +Options: fix, feat +Criteria: + fix = bug, regression, unexpected behavior, crash, or something broken that needs repair + feat = new capability, enhancement, improvement, or something that doesn't exist yet +Example answer: feat +``` + +Interpret the returned label directly. Fall back to the default-to-feat rule if no decision tool is available. + +**Capture the category and label as variables:** + +```bash +# Set CATEGORY and LABEL based on the analysis above +CATEGORY="" +LABEL="" +``` + +### 2.5. Scope Judgment — Check for bundling + +**Before synthesizing the title, check whether the user's description bundles multiple independent capabilities into a single issue.** If it does, recommend splitting before implementation. + +**Judgment criteria — recommend splitting if any of these apply:** + +- **Multiple distinct tools/capabilities** — e.g., "add clipboard, browser automation, and desktop automation" (3 separate tools) +- **Multiple API integrations** — e.g., "add Google Docs, Sheets, Drive, Word, Excel, OneDrive" (6 separate APIs) +- **Multiple capability areas in one tool** — e.g., "add copywriting, SEO, social media, translation, and text extraction" (5+ capability areas) +- **Clear separation of concerns** — e.g., clipboard (simple, cross-platform) vs browser automation (heavy, platform-specific) + +**Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to judge whether to split. Pass the description as state. + +``` +Question: "Does this issue bundle multiple independent capabilities that should be split?" +Type: noul +Criteria: + false = single narrow capability + true = 2+ distinct tools/APIs/capability areas +Example answer: true +``` + +**Important:** The `noul` type returns a *single float in [0,1]*, not a label — it is the probability of `true`. Interpret it as: `≥ 0.6` → true (split), `≤ 0.4` → false (don't split), between → unknown. Fall back to the judgment criteria above if no decision tool is available. + +**Action:** + +- **If the issue is narrow** (one tool, one API, one capability area): proceed to step 3. +- **If the issue is broad** (bundles 2+ distinct capabilities): proceed to step 3 but **append a "Split Recommendation"** section to the synthesized body (see step 4.6). In the final report, note: `Split recommended: `. + +**Do NOT refuse to create the issue.** Create it as requested, but flag the bundling so the implementer can split it during the fix phase. + +### 3. Synthesize Title + +Create a concise, conventional-commit-style title: + +- **Fix:** `fix: ` — e.g., `fix: crash on empty input` +- **Feat:** `feat: ` — e.g., `feat: add file upload endpoint` + +Keep it under 70 characters. Be specific but terse. No trailing punctuation. + +**Capture the title in a variable:** + +```bash +SYNTHESIZED_TITLE=": " +``` + +### 4. Synthesize Description + +Read the appropriate template from disk, then populate every section with synthesized content derived from the user's description. + +**This is purely a grammatical task.** Do not search the codebase, read source files, or look up implementation details. You are rephrasing and structuring what the user has already told you — not investigating the code. The audit (step 6) is where code investigation happens. If you don't know a detail, write `Unknown — user to confirm`. + +**Read the template first** — the skill must load the actual file from `.github/ISSUE_TEMPLATE/` so it stays in sync with whatever is on disk: + +- **fix** → first try `.github/ISSUE_TEMPLATE/bug_report.md`, fall back to `.github/ISSUE_TEMPLATE.md` +- **feat** → first try `.github/ISSUE_TEMPLATE/feature_request.md`, fall back to `.github/ISSUE_TEMPLATE.md` + +**Verify template exists before reading**; fall back gracefully: + +```bash +# Determine which template to use +if [ "$CATEGORY" = "fix" ]; then + TEMPLATE_PATH=".github/ISSUE_TEMPLATE/bug_report.md" +else + TEMPLATE_PATH=".github/ISSUE_TEMPLATE/feature_request.md" +fi + +# Try the specific template first, then fall back to the general one +if [ ! -f "$TEMPLATE_PATH" ]; then + TEMPLATE_PATH=".github/ISSUE_TEMPLATE.md" +fi + +# If nothing exists, report error and stop +if [ ! -f "$TEMPLATE_PATH" ]; then + echo "ERROR: No issue template found. Expected one of:" + echo " .github/ISSUE_TEMPLATE/bug_report.md" + echo " .github/ISSUE_TEMPLATE/feature_request.md" + echo " .github/ISSUE_TEMPLATE.md" + exit 1 +fi +``` + +The template may contain YAML frontmatter — strip it before populating. Use `sed` to remove lines between the first `---` and second `---` portably (with `-e` for multi-command): + +```bash +# Strip YAML frontmatter (lines between first and second ---) +# Two-pass approach for POSIX sed compatibility: +# Pass 1: delete from line 1 through first --- (inclusive) +# Pass 2: delete from the next --- through end of file +sed '1,/^---$/d' "$TEMPLATE_PATH" | sed '1,/^---$/d' +``` + +**Rules for synthesis:** +- Fill **every** section. Never leave a section blank. +- If you cannot infer a detail (e.g., OS, Node version), write `Unknown — user to confirm` rather than guessing. +- The **Summary** should be 1-2 sentences, clear and direct. +- The **Motivation** (for feats) should explain the "why" — the problem being solved. +- The **Proposed Solution** (for feats) should be actionable — what the implementation would look like. +- For fixes, the **Reproduction** steps should be concrete and sequential. +- For fixes, the **Expected** vs **Actual** behavior should be clearly contrasted. + +**For feat issues, append these sections after the Proposed Solution (before Alternatives Considered):** + +- **Dependencies** — Name the npm packages (or system dependencies) the implementation will use, with version ranges and a brief justification. If the user didn't specify, research the most common/active package for the capability. Format: + ```markdown + ## Dependencies + + - **** (v+ — ) + - **System dependency** — + ``` + +- **Testing Strategy** — One paragraph describing how the feature will be tested. Include unit tests, integration tests, and edge cases. Format: + ```markdown + ## Testing Strategy + + - **Unit tests**: + - **Integration test**: + - **Edge cases**: + ``` + +- **Security Considerations** — One paragraph referencing AGENTS.md 1.2 requirements. Include credential management, input validation, and any OWASP-relevant concerns. Format: + ```markdown + ## Security Considerations + + - **Credential storage**: + - **Input validation**: + - **OWASP**: + ``` + +**For fix issues, append these sections after the Expected/Actual contrast:** + +- **Root Cause Analysis** — One paragraph describing the likely root cause based on the user's description. If you cannot determine the root cause from the description alone, write `Unknown — audit required`. +- **Testing Strategy** — One paragraph describing regression tests to prevent recurrence. +- **Security Considerations** — If the fix touches sensitive areas (auth, input handling, file I/O), note OWASP-relevant concerns. + +**Capture the populated template body** — store it in `$FULL_TEMPLATE_BODY` for use in Step 5: + +```bash +# Read the template, strip frontmatter, and store in variable +# Two-pass approach for POSIX sed compatibility: +# Pass 1: delete from line 1 through first --- (inclusive) +# Pass 2: delete from the next --- through end of file +FULL_TEMPLATE_BODY=$(sed '1,/^---$/d' "$TEMPLATE_PATH" | sed '1,/^---$/d') +``` + +### 4.5. Populate Environment Section + +**If the template contains an "Environment" section, populate it with live machine details before creating the issue.** Do this after step 4 (Synthesize Description) and before step 5 (Create the Issue). + +1. **Check for an Environment section** — Scan the populated template body for a section titled `## Environment` (or similar, case-insensitive). If none exists, skip this step. + +2. **Gather machine details:** + - **OS:** Run `uname -s` and `uname -r` to get the OS name and kernel version. Format: `OS_NAME KERNEL_VERSION` (e.g., `Linux 7.0.2-7-pve`). + - **Node.js:** Run `node --version` to get the Node version (e.g., `v25.8.1`). + - **madz version:** Read `package.json` from the project root and extract the `version` field. Use `node -e "console.log(require('./package.json').version)"`. + - **LLM provider:** If the user's description mentions a specific provider (e.g., OpenAI, Anthropic), use that. Otherwise write `Unknown — user to confirm`. + +3. **Replace placeholder values** — In the template body, replace the Environment section's placeholder lines with the actual values. Typical placeholders look like: + ``` + - **OS**: (e.g., macOS 14.5, Ubuntu 24.04, Windows 11) + - **Node.js**: (e.g., 24.2.0) + - **madz version**: (e.g., 1.7.3 — check `npm list @avoidwork/madz` or Docker tag) + - **LLM provider**: (e.g., OpenAI gpt-4o, Anthropic claude-3.5-sonnet) + ``` + Replace each line's placeholder with the actual value. Preserve the `**KEY**:` format. + +4. **Handle `#` characters carefully** — GitHub interprets `#` as a link trigger in certain contexts. When writing values, **always omit `#` in inline content**: + + - ❌ `issue #290` in prose → ✅ `issue 290` in prose + - ❌ `line #42` → ✅ `line 42` + - ❌ `v25#8` → ✅ `v25.8` + + **Exception:** Issue references in the form `#` (e.g., `resolves #42`, `see #17`) are **preserved** and SHOULD NOT be stripped — GitHub uses them to create clickable links to those issues. This rule applies only to stray `#` characters in content, not to valid GitHub issue references. + +5. **Write the updated body** to the temp file before proceeding to step 5. + +### 4.6. Append Split Recommendation (if scope judgment triggered) + +**If step 2.5 flagged bundling, append a "Split Recommendation" section to the issue body.** This goes after the Environment section (step 4.5) and before the OpenSpec Note. + +```markdown +## Split Recommendation + +This issue bundles multiple distinct capabilities. Consider splitting into separate issues: + +- **** — +- **** — +- **** — + +**MVP recommendation:** Start with as it has the least dependencies and highest immediate value. +``` + +**Write the updated body** to the temp file before proceeding to step 5. + +**Example transformation:** +``` +Before: +## Environment +- **OS**: (e.g., macOS 14.5, Ubuntu 24.04, Windows 11) +- **Node.js**: (e.g., 24.2.0) +- **madz version**: (e.g., 1.7.3) +- **LLM provider**: (e.g., OpenAI gpt-4o) + +After: +## Environment +- **OS**: Linux 7.0.2-7-pve +- **Node.js**: v25.8.1 +- **madz version**: 1.7.5 +- **LLM provider**: OpenAI gpt-4o +``` + +### 5. Create the Issue — **EXECUTE THIS BEFORE ANY CODE SEARCH** + +**This is the hard stop.** You must create the GitHub issue *now*, with nothing more than the synthesized title and description. No codebase search. No audit. No looking at files. The issue must exist on GitHub before you touch a single source file. + +**Determine the target repository dynamically from the git remote.** Never hardcode a repo name: + +```bash +# Extract owner/repo from git remote URL — handles both HTTPS and SSH +GIT_REMOTE=$(git remote get-url origin 2>/dev/null) +if [ -z "$GIT_REMOTE" ]; then + echo "ERROR: No git remote 'origin' found. Cannot determine repository." + exit 1 +fi + +GH_REPO_FLAG="" +# SSH format: git@github.com:owner/repo.git +if echo "$GIT_REMOTE" | grep -q '^git@'; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\)\.git$/\1/') + if [ -z "$GH_REPO" ]; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\)$/\1/') + fi + GH_REPO_FLAG="--repo $GH_REPO" +# HTTPS format: https://github.com/owner/repo.git or https://TOKEN@github.com/owner/repo.git +elif echo "$GIT_REMOTE" | grep -q 'github\.com'; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\)\.git$/\1/') + if [ -z "$GH_REPO" ]; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\)$/\1/') + fi + GH_REPO_FLAG="--repo $GH_REPO" +fi + +if [ -z "$GH_REPO" ]; then + echo "ERROR: Could not parse repository from remote '$GIT_REMOTE'. Cannot create issue." + exit 1 +fi + +# Validate GH_REPO is non-empty and contains a slash (owner/repo format) +if ! echo "$GH_REPO" | grep -q '/'; then + echo "ERROR: Parsed repository '$GH_REPO' does not look like 'owner/repo'. Check remote URL." + exit 1 +fi +``` + +**Execution order is non-negotiable:** +1. Synthesize title and description (steps 2–4) +2. Populate Environment section if present (step 4.5) +3. **Create the issue on GitHub** (this step) +4. *Then and only then* proceed to the audit (step 6) + +**Cleanup:** Remove `BODY_FILE` once the issue is created, or when cleaning up after a failure. Do not rely on a shell `trap` — delete the temp file explicitly when the task completes. + +```bash +BODY_FILE=$(mktemp) + +cat > "$BODY_FILE" << BODYEOF +$(echo "$FULL_TEMPLATE_BODY" | sed "s||$SYNTHESIZED_TITLE|g") +BODYEOF + +ISSUE_URL=$(gh issue create \ + --title "$SYNTHESIZED_TITLE" \ + --body-file "$BODY_FILE" \ + --label "$LABEL" \ + $GH_REPO_FLAG) + +ISSUE_NUMBER=$(echo "$ISSUE_URL" | sed 's/.*\/issues\/\([0-9]\{1,\}\).*/\1/') + +# Verify the issue was created successfully and is open +ISSUE_STATE=$(gh issue view "$ISSUE_NUMBER" --json state --jq '.state' $GH_REPO_FLAG 2>/dev/null || true) +if [ "$ISSUE_STATE" != "OPEN" ]; then + echo "ERROR: Issue #$ISSUE_NUMBER was created but state is '$ISSUE_STATE' (expected OPEN). Aborting." + rm -f "$BODY_FILE" + exit 1 +fi +echo "Issue #$ISSUE_NUMBER verified as OPEN." + +rm -f "$BODY_FILE" +``` + +**Important:** Replace all `` tokens with actual variable values before executing. Never pass literal placeholder strings to `gh` commands. + +**STOP here.** The issue is created. Report the number and URL. Then proceed to step 6. + +### 6. Light Audit — Gather actionable context + +*Now* that the issue exists, do a **targeted, lightweight** audit. **You must use the `ISSUE_NUMBER` returned in Step 5.** Anchor every finding to this specific issue. The goal is to surface useful context — not to solve the problem or write a treatise. + +#### For **fix** issues — quick root-ause hunt: + +1. **Search for relevant code** — Use `searchFiles` to find files, functions, or patterns related to the bug. +2. **Read the most relevant file** — Open one or two key files. Look for the code that handles the reported scenario. +3. **Note what you find** — File paths, line numbers, function names, the likely culprit. Keep it brief. Explicitly reference `#` when documenting findings. + +#### For **feat** issues — quick landscape scan: + +1. **Search for existing related functionality** — What's already there? +2. **Identify integration points** — Where would this plug in? +3. **Note relevant files** — Which files would need changes? Explicitly reference `#` when documenting findings. + +**Time budget:** Keep this to a few focused searches and reads. If the audit yields nothing obvious, move on — don't spin your wheels. + +#### Audit output format: + +```markdown +## Audit Findings (for Issue #) + +- **File**: `path/to/file.ts` — +- `path/to/related.ts` — +- +``` + +### 6.5 Update Issue with Audit Findings + +**After the audit (step 6), append the findings to the issue body.** Do not skip this step — the audit notes must be visible on the issue. + +1. **Build the audit section** using the format from step 6. + +2. **Append to the existing issue body** using `gh issue edit`. Read the current body, append the audit section, and write the combined body back: + +```bash +# Read current body +CURRENT_BODY=$(gh issue view --json body --jq '.body') + +# Build the audit section +AUDIT_SECTION=" +## Audit Findings (for Issue #${ISSUE_NUMBER}) + +- **** — +- +" + +# Append and update — write CURRENT_BODY + AUDIT_SECTION to temp file +AUDIT_FILE=$(mktemp) +cat > "$AUDIT_FILE" << AUDITEOF +${CURRENT_BODY} +${AUDIT_SECTION} +AUDITEOF + +gh issue edit "$ISSUE_NUMBER" --body-file "$AUDIT_FILE" $GH_REPO_FLAG +rm -f "$AUDIT_FILE" +``` + +3. **Verify the audit notes are present.** Read the issue back and confirm the audit section exists: + +```bash +VERIFY=$(gh issue view "$ISSUE_NUMBER" --json body --jq '.body' $GH_REPO_FLAG) +if echo "$VERIFY" | grep -q "Audit Findings"; then + echo "Audit notes verified on issue." +else + echo "WARNING: Audit notes not found on issue. Retrying..." + # Retry once with the same append logic + printf '%s\n' "${CURRENT_BODY}${AUDIT_SECTION}" | gh issue edit "$ISSUE_NUMBER" --body-file - $GH_REPO_FLAG +fi +``` + +4. **If verification fails after retry**, report the failure in the final summary but do not block — the issue still exists. + +### 6.5.1 Handle Large Audit Findings + +GitHub issue bodies have a 65,536 character limit. If your audit findings are large: +- Keep findings concise — one line per file, no verbose explanations +- If findings exceed 10,000 characters, summarize: list files with issues and note "Full details in attached log" (then attach if possible) +- Never truncate mid-sentence — always end with a complete thought + +### 6.6. Fix Steps — Actionable implementation guidance + +**After the audit (step 6), append a "Fix Steps" section to the issue body.** This gives the implementer a clear, sequential path from zero to working code. Do not skip this step — it is what makes the issue actionable. + +**For fix issues, the Fix Steps should include:** + +```markdown +## Fix Steps + +1. **Locate the bug** — Open `path/to/file.ts` and find the code at line that causes the issue. +2. **Add validation** — Insert a check at line that validates before . +3. **Handle the error** — Return a user-friendly error message instead of crashing. +4. **Add a test** — Create `tests/unit/path/to/file.test.js` with a test case for the edge case. +5. **Verify** — Run `npm run test` and `npm run coverage` to confirm no regressions. +``` + +**For feat issues, the Fix Steps should include:** + +```markdown +## Fix Steps + +1. **Create the tool file** — Add `src/tools//index.js` with a Zod schema and impl function. +2. **Register the tool** — Add the tool to `src/tools/index.js` with the appropriate permissions. +3. **Write unit tests** — Add `tests/unit/tools/.test.js` covering schema validation and core logic. +4. **Write integration test** — Add `tests/integration/tools/.test.js` covering end-to-end behavior. +5. **Update documentation** — Add JSDoc comments to all public functions with `@param` and `@returns`. +6. **Verify** — Run `npm run test`, `npm run lint`, and `npm run coverage` to confirm everything passes. +``` + +**General rules for Fix Steps:** +- Be specific — reference actual file paths, line numbers, and function names from the audit. +- Be sequential — each step should build on the previous one. +- Include verification — always end with a step that confirms the fix/feature works (tests, lint, coverage). +- If the audit didn't yield specific file paths, use placeholders: `src/tools//index.js`, `tests/unit/tools/.test.js`. +- Keep it to 5-7 steps max. If more are needed, group related steps. + +### 7. Report + +Print a final summary: + +``` +Created issue #: +URL: <URL> +Category: <fix|feat> +Audit: <brief note on what was found — e.g., "root cause identified in callHandler.ts:142" or "mapped 3 integration points for proposed feature"> +``` + +## Error Handling + +- **Not authenticated:** Suggest running `gh auth login`. +- **Network error:** Retry once, then report the failure. +- **Template not found:** Report the error and stop — do not proceed without the template. +- **GitHub API error:** Report the specific error message and stop. +- **Audit yields nothing:** If the codebase search returns no relevant results, note "No relevant code paths found — issue may be environment-specific or in external dependencies." and still report the issue creation. + +## Gotchas + +- **Issue creation is the hard stop.** The issue must exist on GitHub before any codebase audit. Never search the codebase before creating the issue. +- **`#` characters in content are link triggers.** GitHub interprets `#` as a link trigger in certain contexts. Omit `#` in inline content (e.g., `line 42` not `line #42`), but preserve valid issue references like `resolves #42`. +- **Label discipline is strict.** Only `bug` and `feature` labels are valid. Never use `enhancement`, `improvement`, `refactor`, `docs`, or any other label. +- **Environment section must be populated.** If the template has an Environment section, always fill it with live machine details — never leave placeholders. + +## Example + +``` +User: create-issue the TUI hangs when the user types a very long command and presses enter + +Agent: Categorizing... This is a fix — the TUI should handle long input gracefully. + +Synthesizing title: fix: TUI hangs on long command input + +Filling bug_report template... + Summary: The TUI freezes when a user enters a command exceeding a certain length and presses Enter. + Reproduction: 1. Start madz TUI 2. Type a very long command (e.g., 500+ characters) 3. Press Enter + Expected: The command is processed or truncated gracefully. + Actual: The TUI becomes unresponsive and requires a force quit. + Root Cause: Unknown — audit required + Testing: Add regression test for long input in src/tui/inputPanel.test.js + Security: Input validation prevents buffer overflow and DoS + +Creating issue... +Created issue #256: fix: TUI hangs on long command input +URL: https://github.com/avoidwork/madz/issues/256 + +Auditing codebase... + Found input handler in src/tui/inputPanel.ts:87 — no length validation before dispatch. + Related: src/tui/provider.ts handles command dispatch but assumes valid input. + +Updating issue with audit findings and fix steps... + +Created issue #256: fix: TUI hangs on long command input +URL: https://github.com/avoidwork/madz/issues/256 +Category: fix +Audit: Root cause in src/tui/inputPanel.ts:87 — no length validation before dispatch +Fix: 5 steps provided — locate bug, add validation, handle error, add test, verify +``` + +## Example — Broad Scope (Split Recommended) + +``` +User: create-issue add Google Docs, Sheets, Drive, Word, Excel, and OneDrive integration + +Agent: Categorizing... This is a feat — new capability. +Scope judgment: Bundles 6 distinct API integrations. Recommend splitting. + +Synthesizing title: feat: add Google Workspace / Microsoft 365 API integration + +Filling feature_request template... + Summary: Add tools for interacting with Google Workspace (Docs, Sheets, Drive) and Microsoft 365 (Word, Excel, OneDrive) via their respective APIs. + Motivation: Real office workflows live in cloud productivity suites — users need to create, edit, and manage documents in these platforms directly. + Proposed Solution: Create API integration tools for Google Docs, Sheets, Drive, Microsoft Word, Excel, and OneDrive. + Dependencies: googleapis (v144.x), @microsoft/microsoft-graph-client (v3.x), google-auth-library (v9.x), @azure/identity (v4.x), better-sqlite3 (v11.x) + Testing: Unit tests with mocked OAuth, integration tests with sandbox environments, edge cases for large docs and rate limits + Security: Encrypted token storage, least-privilege scopes, rate limiting, PII handling + +Creating issue... +Created issue #783: feat: add Google Workspace / Microsoft 365 API integration +URL: https://github.com/avoidwork/madz/issues/783 + +Auditing codebase... + No existing cloud API tools found — this is a greenfield feature. + Integration point: src/tools/index.js for tool registration. + OAuth infrastructure: none exists yet — needs to be built first. + +Updating issue with audit findings, dependencies, testing, security, and fix steps... + +Created issue #783: feat: add Google Workspace / Microsoft 365 API integration +URL: https://github.com/avoidwork/madz/issues/783 +Category: feat +Audit: Greenfield — no existing cloud API tools, OAuth infrastructure needed first +Split recommended: 6 APIs bundled — start with Google Docs + Sheets as MVP +Fix: 6 steps provided — create tool files, register, test, document, verify +``` + +--- diff --git a/.skills/fix-issue/SKILL.md b/.skills/fix-issue/SKILL.md new file mode 100644 index 000000000..dabf76400 --- /dev/null +++ b/.skills/fix-issue/SKILL.md @@ -0,0 +1,301 @@ +--- +name: fix-issue +description: Accepts a GitHub issue ID, validates approval via labels, categorizes the work, chains to /create-feature for full implementation, and comments on the issue linking to the PR. +license: BSD 3-Clause +compatibility: Requires gh CLI authenticated with the repo. Must be run from the project root. +metadata: + agent: coding +--- + +# Fix Issue + +> **⚠️ EXECUTION RULE:** Every code block in this skill is a shell command to **execute**. Do not print them as text, explain them, or treat them as examples — run them directly. + +Accepts a GitHub issue ID, validates it's approved for work, categorizes the effort, and chains to `/create-feature` for full implementation. + +## Pipeline + +This is an automatic pipeline. Execute each step in order without stopping for confirmation or narration. When you reach step 6, chain to `/create-feature` and wait for it to complete. Do not describe the chain — execute it. + +### Step 1: Parse Input + +The issue ID is provided in the command context (the text after `/fix-issue`). **Use it directly.** Do not ask the user for it. It may be provided as: +- A bare number: `42` +- With a prefix: `#42` or `issue-42` + +Strip any non-numeric characters and extract the numeric ID. If no ID was provided in the command, *then* ask the user for it. + +```bash +# Extract issue number from chain context (text after /fix-issue) +# The chain context is passed via CHAIN_CONTEXT env var or as $1 +ISSUE_NUM="${CHAIN_CONTEXT:-$1}" +# Strip non-numeric characters (handles #42, issue-42, etc.) +ISSUE_NUM=$(echo "$ISSUE_NUM" | sed 's/[^0-9]//g') +if [ -z "$ISSUE_NUM" ]; then + echo "ERROR: No issue number provided. Usage: fix-issue <number>" + exit 1 +fi +echo "ISSUE_NUM=$ISSUE_NUM" +``` + +### Step 2: Fetch Issue Details + +Determine the target repository dynamically from the git remote. Never hardcode a repo name: + +```bash +# Extract owner/repo from git remote URL — handles both HTTPS and SSH +GIT_REMOTE=$(git remote get-url origin 2>/dev/null) +if [ -z "$GIT_REMOTE" ]; then + echo "ERROR: No git remote 'origin' found. Can't determine repository." + exit 1 +fi + +# SSH format: git@github.com:owner/repo.git +if echo "$GIT_REMOTE" | grep -q '^git@'; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\)\.git$/\1/') + # Handle remote without .git suffix + if [ -z "$REPO" ]; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\)$/\1/') + fi +# HTTPS format: https://github.com/owner/repo.git +elif echo "$GIT_REMOTE" | grep -q 'github\.com'; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\)\.git$/\1/') + # Handle remote without .git suffix + if [ -z "$REPO" ]; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\)$/\1/') + fi +else + echo "ERROR: Could not parse repository from remote '$GIT_REMOTE'." + exit 1 +fi + +# Validate REPO is non-empty and contains a slash (owner/repo format) +if [ -z "$REPO" ] || ! echo "$REPO" | grep -q '/'; then + echo "ERROR: Parsed repository '$REPO' does not look like 'owner/repo'. Check remote URL." + exit 1 +fi +``` + +```bash +ISSUE_DETAILS=$(gh issue view "$ISSUE_NUM" --json title,body,labels,state,url --repo "$REPO") +echo "$ISSUE_DETAILS" +``` + +**Check if issue is closed:** After fetching, inspect the `state` field. If `state` is `"CLOSED"`, stop and report: "Issue #$ISSUE_NUM is closed. No action taken." + +If the issue does not exist or is inaccessible, report the error and stop. + +### Step 3: Validate Approval + +Inspect the `labels` array from the issue JSON. + +- **If `in progress` label is present:** Stop. Inform the user that the issue is already being worked on. Provide the issue URL. +- **If `approved` label is present:** Proceed. +- **If neither `in progress` nor `approved` is present:** Stop. Inform the user that the issue is not approved for work. Provide the issue URL and suggest they add the label before proceeding. + +``` +Issue #<ID> is not approved for work. +Label "approved" not found. +URL: <url> +Please add the "approved" label before requesting implementation. +``` + +``` +Issue #<ID> is already being worked on. +Label "in progress" found. +URL: <url> +No action taken. +``` + +### Step 4: Set In-Progress Label + +Before any work begins, mark the issue as being worked on: + +```bash +gh issue edit "$ISSUE_NUM" --add-label "in progress" --repo "$REPO" + +# Verify the label was added successfully +LABEL_CHECK=$(gh issue view "$ISSUE_NUM" --json labels --jq '.labels[].name' --repo "$REPO" 2>/dev/null || true) +if ! echo "$LABEL_CHECK" | grep -q 'in progress'; then + echo "ERROR: Failed to add 'in progress' label to issue #$ISSUE_NUM." + exit 1 +fi +echo "Label 'in progress' verified on issue #$ISSUE_NUM." +``` + +This ensures the issue won't be picked up again by `scan-issues` and clearly marks it as being worked on. (Note: `--add-label` is idempotent — adding an already-present label is a no-op.) + +### Step 5: Categorize the Work + +Inspect the labels for a categorization tag. Priority order: + +1. `bug` — defect or unexpected behavior +2. `feature` — new capability +3. `chore` — maintenance, cleanup, refactoring +4. `docs` — documentation changes +5. `test` — test coverage improvements + +If no categorization label is found, **default to `bug`**. + +**Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to categorize instead of reasoning it out manually. Pass the issue details as state. + +``` +Question: "What category is this issue?" +Type: choice +Options: bug, feature, chore, docs, test +Criteria: + bug = defect or unexpected behavior + feature = new capability + chore = maintenance, cleanup, refactoring + docs = documentation changes + test = test coverage improvements +Example answer: bug +``` + +Interpret the returned label directly. Fall back to the priority order above if no decision tool is available. + +### Step 6: Check for OpenSpec / Package Manager / Build Process + +Before invoking `create-feature`, verify whether the project has the infrastructure that `create-feature` depends on: + +```bash +# Check for package.json (Node.js project) +HAS_PACKAGE_JSON=false +if [ -f "package.json" ]; then + HAS_PACKAGE_JSON=true +fi + +# Check for openspec/ directory +HAS_OPENSPEC=false +if [ -d "openspec" ]; then + HAS_OPENSPEC=true +fi + +# Check for build/start scripts in package.json +HAS_BUILD_SCRIPT=false +if [ "$HAS_PACKAGE_JSON" = true ]; then + BUILD_SCRIPTS=$(node -e "console.log(Object.keys(require('./package.json').scripts || {}))" 2>/dev/null) + if echo "$BUILD_SCRIPTS" | grep -qE '"(start|build|test|lint|coverage)"'; then + HAS_BUILD_SCRIPT=true + fi +fi + +echo "HAS_PACKAGE_JSON=$HAS_PACKAGE_JSON" +echo "HAS_OPENSPEC=$HAS_OPENSPEC" +echo "HAS_BUILD_SCRIPT=$HAS_BUILD_SCRIPT" +``` + +**If all three are absent** (`HAS_PACKAGE_JSON=false`, `HAS_OPENSPEC=false`, `HAS_BUILD_SCRIPT=false`), the project has no package manager, no OpenSpec workflow, and no build process. In this case, invoke `create-feature` with a `SKIP_OPENSPEC` directive that tells it to skip all OpenSpec-related steps (proposal, spec generation, task breakdown, archival) and proceed directly to: + +1. Create the feature branch +2. Implement the fix inline +3. Run tests only if a `test` script exists in package.json (even if no build scripts, a test script may exist) +4. Commit and push +5. Create the PR +6. Post audit comment on the issue + +Pass `SKIP_OPENSPEC=true` in the chain instruction to `create-feature`. + +**If any of the three are present**, proceed with the normal `create-feature` invocation (full pipeline with OpenSpec). + +### Step 7: Run create-feature + +**This is not a description. Execute it.** + +Map the categorized type to a conventional commit prefix for the branch name: + +| Category | Branch Type | +|----------|-------------| +| `bug` | `fix` | +| `feature`| `feat` | +| `chore` | `chore` | +| `docs` | `docs` | +| `test` | `test` | + +Invoke the `create-feature` skill as a chain instruction (text delegation), passing the branch type via `BRANCH_TYPE` and the `SKIP_OPENSPEC` flag if applicable. Keep the instruction under 300 characters. + +If `SKIP_OPENSPEC` was set in Step 6, include `SKIP_OPENSPEC=true` in the chain instruction. Otherwise, omit it. + +**Wait for the invocation to complete.** Do not proceed to Step 7 until the feature implementation, testing, PR creation, and archival are all done. The `create-feature` skill handles: + +- Proposal generation +- Spec design +- Task breakdown +- Todo item creation +- Implementation +- Testing +- PR creation +- Archival +- Prints structured output: `PR_NUMBER=<number>` and `PR_URL=<url>` + +**Read the `PR_NUMBER` from the structured output printed by `create-feature`.** Do not attempt to grep a shell variable — the value is in the conversation history. + +Add a reusable capture pattern to extract `PR_NUMBER` from the conversation history after `create-feature` completes: + +```bash +# Capture PR_NUMBER from the conversation history (last occurrence wins) +# The chained skill prints lines like: PR_NUMBER=42 or PR_URL=https://... +# Use grep to find lines matching the pattern, take the last one +PR_NUMBER=$(echo "$CONVERSATION_HISTORY" | sed -n 's/^PR_NUMBER=\([0-9]\{1,\}\)$/\1/p' | tail -1) +if [ -z "$PR_NUMBER" ]; then + echo "ERROR: Could not capture PR_NUMBER from create-feature output. Conversation history:" + echo "$CONVERSATION_HISTORY" | tail -20 + exit 1 +fi +echo "PR_NUMBER=$PR_NUMBER" +``` + +**Do NOT create todo items in this skill.** Todo management is the sole responsibility of `create-feature`. + +**If create-feature fails:** Log the error, skip Step 8 (commenting), and report the failure in the final summary. Do not attempt to recover. + +### Step 8: Comment on Issue + +**Skip this step if create-feature failed.** If create-feature failed (per Step 7 error handling), do not attempt to comment — the PR was never created. + +Use the `PR_NUMBER` from the structured output printed by `create-feature` in Step 6. Do not attempt to extract it from a shell variable — it is in the conversation history. + +```bash +gh issue comment "$ISSUE_NUM" --repo "$REPO" --body "Fixed in #${PR_NUMBER}." + +# Verify the comment was posted successfully +COMMENT_CHECK=$(gh issue view "$ISSUE_NUM" --json comments --jq '.comments[-1].body' --repo "$REPO" 2>/dev/null || true) +if ! echo "$COMMENT_CHECK" | grep -q "Fixed in #${PR_NUMBER}"; then + echo "WARNING: Failed to verify comment on issue #$ISSUE_NUM." +fi +``` + +If the comment fails, note it in the final report but do not stop. + +## Error Handling + +- **Issue not found:** Report the error clearly with the provided ID. +- **Not authenticated:** Suggest running `gh auth login`. +- **Network error:** Retry once, then report the failure. +- **`/create-feature` failure:** Report the specific failure and continue with what was accomplished. Never leave the queue half-done. + +## Gotchas + +- **Approval is mandatory.** The skill stops immediately if the issue lacks the `approved` label. Do not attempt to bypass this check. +- **`in progress` label prevents re-processing.** If the label is present, the skill reports and exits — the issue is already being worked on. +- **PR number comes from create-feature output.** The `create-feature` skill prints `PR_NUMBER=<number>` as structured output. Read it from the conversation history. +- **Todo management is delegated.** The `create-feature` skill owns todo items. Do not create todos in this skill. + +## Example + +``` +User: fix-issue 123 + +Agent: Fetching issue #123... + Title: "Crash on empty input" + Labels: approved, bug + State: open + + Issue is approved. Category: bug. + + Invoking create-feature... + + [waits for create-feature to complete] + + Commenting on issue #123 → Fixed in #126. +``` \ No newline at end of file diff --git a/.skills/git-tag/SKILL.md b/.skills/git-tag/SKILL.md new file mode 100644 index 000000000..07e475422 --- /dev/null +++ b/.skills/git-tag/SKILL.md @@ -0,0 +1,328 @@ +--- +name: git-tag +description: Reads version from package.json (Node.js), pyproject.toml (Python), Cargo.toml (Rust), build.gradle (Java/Android), pom.xml (Maven), build.gradle.kts (Gradle Kotlin), pubspec.yaml (Dart), Gemfile (Ruby), or similar package manager files. Synthesizes a change description from git history delta, creates an annotated git tag (no "v" prefix), and pushes it to origin. +license: BSD 3-Clause +compatibility: Requires git CLI configured with remote access and a supported package manager file with a "version" field in the working directory (package.json, pyproject.toml, Cargo.toml, build.gradle, pom.xml, build.gradle.kts, pubspec.yaml, Gemfile, etc.). +metadata: + agent: coding +--- + +# Git Tag + +> **⚠️ EXECUTION RULE:** Every code block in this skill is a shell command to **execute**. Do not print them as text, explain them, or treat them as examples — run them directly. + +You must execute ALL steps in order. Do not stop until the tag is pushed and verified on the remote. If any step fails, report the error and stop — do not continue with incomplete work. + +## Step 1: Read the Version + +Detect the package manager and extract the version string. Try each file in order until one is found: + +### 1a. Node.js (package.json) + +If `package.json` exists, use `node` to reliably extract the top-level `version` field (avoids matching nested "version" keys): + +```bash +if [ -f package.json ]; then + TAG_VERSION=$(node -e "console.log(require('./package.json').version)") + echo "TAG_VERSION=$TAG_VERSION" + VERSION_SOURCE="package.json" +fi +``` + +If `node` is unavailable, fall back to grep: +```bash +TAG_VERSION=$(grep -m1 '"version"[[:space:]]*:' package.json | sed 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/') +``` + +### 1b. Python (pyproject.toml) + +If no `package.json` exists and `pyproject.toml` exists, extract the version from the `[project]` table: + +```bash +if [ -z "$TAG_VERSION" ] && [ -f pyproject.toml ]; then + # Try inline version field under [project] + TAG_VERSION=$(grep -m1 '^version[[:space:]]*=' pyproject.toml | sed 's/^version[[:space:]]*=[[:space:]]*"\?\([^"]*\)"\?.*/\1/' | tr -d "'") + if [ -n "$TAG_VERSION" ]; then + echo "TAG_VERSION=$TAG_VERSION" + VERSION_SOURCE="pyproject.toml" + fi +fi +``` + +### 1c. Rust (Cargo.toml) + +If no version found yet and `Cargo.toml` exists: + +```bash +if [ -z "$TAG_VERSION" ] && [ -f Cargo.toml ]; then + TAG_VERSION=$(grep -m1 '^version[[:space:]]*=' Cargo.toml | sed 's/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/') + if [ -n "$TAG_VERSION" ]; then + echo "TAG_VERSION=$TAG_VERSION" + VERSION_SOURCE="Cargo.toml" + fi +fi +``` + +### 1d. Java/Android/Gradle (build.gradle or build.gradle.kts) + +If no version found yet and a Gradle build file exists: + +```bash +if [ -z "$TAG_VERSION" ]; then + if [ -f build.gradle ]; then + # groovy: version '1.2.3' or version = '1.2.3' + TAG_VERSION=$(grep -m1 -E "^\s*(version\s*[=:]?\s*['\"]|version\s+['\"])" build.gradle | sed "s/.*['\"]\\([0-9][0-9a-zA-Z._-]*\\)['\"].*/\\1/") + VERSION_SOURCE="build.gradle" + elif [ -f build.gradle.kts ]; then + # kotlin: version = "1.2.3" + TAG_VERSION=$(grep -m1 'version' build.gradle.kts | sed 's/.*version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/' | head -1) + VERSION_SOURCE="build.gradle.kts" + fi + if [ -n "$TAG_VERSION" ]; then + echo "TAG_VERSION=$TAG_VERSION" + fi +fi +``` + +### 1e. Java/Maven (pom.xml) + +If no version found yet and `pom.xml` exists: + +```bash +if [ -z "$TAG_VERSION" ] && [ -f pom.xml ]; then + # Extract project version — skip parent <version> by looking for + # <version> directly under <project> (not under <parent>) + TAG_VERSION=$(grep -A2 '<project>' pom.xml | grep -m1 '<version>' | sed 's|.*<version>\([^<]*\)</version>.*|\1|') + if [ -z "$TAG_VERSION" ]; then + # Fallback: first <version> that is NOT inside <parent>...</parent> + TAG_VERSION=$(sed -n '/<parent>/,/<\/parent>/!p' pom.xml | grep -m1 '<version>' | sed 's|.*<version>\([^<]*\)</version>.*|\1|') + fi + if [ -n "$TAG_VERSION" ]; then + echo "TAG_VERSION=$TAG_VERSION" + VERSION_SOURCE="pom.xml" + fi +fi +``` + +### 1f. Dart (pubspec.yaml) + +If no version found yet and `pubspec.yaml` exists: + +```bash +if [ -z "$TAG_VERSION" ] && [ -f pubspec.yaml ]; then + TAG_VERSION=$(grep -m1 '^version:' pubspec.yaml | sed 's/^version:[[:space:]]*//' | sed 's/[[:space:]]*$//' | cut -d'+' -f1) + if [ -n "$TAG_VERSION" ]; then + echo "TAG_VERSION=$TAG_VERSION" + VERSION_SOURCE="pubspec.yaml" + fi +fi +``` + +### 1g. Ruby (.gemspec) + +If no version found yet, check Gemfile or gemspec: + +```bash +if [ -z "$TAG_VERSION" ]; then + # Use a loop to handle glob safely (avoids issues with zero or multiple matches) + for gemspec in *.gemspec; do + if [ -f "$gemspec" ]; then + TAG_VERSION=$(grep -m1 '\.version[[:space:]]*=' "$gemspec" | sed 's/.*\.version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/' | head -1) + if [ -n "$TAG_VERSION" ]; then + echo "TAG_VERSION=$TAG_VERSION" + VERSION_SOURCE="$gemspec" + fi + fi + break + done +fi +``` + +### 1h. Swift (Package.swift) + +If no version found yet and `Package.swift` exists: + +```bash +if [ -z "$TAG_VERSION" ] && [ -f Package.swift ]; then + TAG_VERSION=$(grep -m1 'package\.version' Package.swift | sed 's/.*\.version(\s*"\([^"]*\)".*/\1/' | head -1) + if [ -n "$TAG_VERSION" ]; then + echo "TAG_VERSION=$TAG_VERSION" + VERSION_SOURCE="Package.swift" + fi +fi +``` + +### Fallback + +If no version was found from any file, report an error and stop. Do not proceed with no version. + +## Step 2: Check Pre-conditions + +Before proceeding, verify: + +```bash +# Ensure we're on a clean working tree +git status --porcelain +``` + +If there are uncommitted changes, report them to the user and stop. Do not tag a dirty tree. + +**Check for detached HEAD:** If `git branch --show-current` returns empty, you're in detached HEAD state. Create a temporary branch first, and clean it up after the tag is pushed: +```bash +WAS_DETACHED=false +if [ -z "$(git branch --show-current)" ]; then + echo "WARNING: Detached HEAD state. Creating temporary branch for tagging." + git checkout -b "tag-temp-$(date -u +%Y%m%d%H%M%S)" + WAS_DETACHED=true +fi +``` + +## Step 3: Find the Previous Tag + +Find the closest prior tag to use as the delta baseline: + +```bash +# Try to find previous tag in same minor version series +# Note: sort -V requires GNU sort; fall back to sort -t. -k1,1n -k2,2n -k3,3n if unavailable +PREV_TAG=$(git tag -l "${TAG_VERSION%.*}.*" 2>/dev/null | sort -V 2>/dev/null | grep -v "^${TAG_VERSION}$" | tail -1) +if [ -z "$PREV_TAG" ]; then + PREV_TAG=$(git tag -l "${TAG_VERSION%.*}.*" 2>/dev/null | sort -t. -k1,1n -k2,2n -k3,3n 2>/dev/null | grep -v "^${TAG_VERSION}$" | tail -1) +fi +if [ -z "$PREV_TAG" ]; then + PREV_TAG=$(git describe --tags --abbrev=0 HEAD 2>/dev/null || echo "") +fi +echo "PREV_TAG=$PREV_TAG" +``` + +If no previous tag exists, set `PREV_TAG=""` (empty). + +## Step 4: Check for Existing Tag + +Ensure the tag does not already exist locally or on the remote: + +```bash +LOCAL_EXISTS=$(git tag -l "${TAG_VERSION}") +REMOTE_EXISTS=$(git ls-remote --tags origin "${TAG_VERSION}" 2>/dev/null) +``` + +If either `LOCAL_EXISTS` or `REMOTE_EXISTS` is non-empty, the tag already exists. Report this to the user and stop. Do not overwrite. + +## Step 5: Gather Commits for Description + +Collect the commit messages between the previous tag and HEAD. **Filter out merge commits** (messages starting with "Merge ") as they add noise. Capture into `COMMITS` for use by Step 6: + +```bash +if [ -n "$PREV_TAG" ]; then + COMMITS=$(git log --pretty=format:"%s" "${PREV_TAG}..HEAD" | grep -v "^Merge ") +else + COMMITS=$(git log --pretty=format:"%s" --max-count=20 | grep -v "^Merge ") +fi +echo "COMMITS=$COMMITS" | cut -c1-2000 +``` + +## Step 6: Synthesize the Description + +Read the commit messages from Step 5 and write a concise, natural-language summary: + +- **Group by topic** — Use conventional commit prefixes to group: `feat:` → features, `fix:` → fixes, `chore:` → chores, `docs:` → documentation, `refactor:` → refactors. If no conventional commit prefixes are present, group by keyword matching (e.g., "Dockerfile" → Docker, "config.yaml" → config). +- **2-4 sentences.** Present tense. Specific but not verbose. +- **No commit hashes or PR numbers.** Natural language only. +- **First line** is the release identifier (e.g., "Version 1.3.5 release."). +- **Remaining lines** describe what changed. + +Example output format: +``` +Version 1.3.5 release. + +Includes a Dockerfile fix for the HOME environment variable, expanded scheduler documentation with atomicity directives, and a TUI audit fix proposal. Documentation updates cover config.yaml tracking, TUI command expansion, and scheduler documentation improvements. +``` + +Then synthesize and set `DESCRIPTION`: + +```bash +if [ -z "$DESCRIPTION" ]; then + DESCRIPTION="Version ${TAG_VERSION} release." +fi +``` + +If no commits can be analyzed, set DESCRIPTION to: + +```bash +DESCRIPTION="Version ${TAG_VERSION} release." +``` + +## Step 7: Create the Tag + +Create an annotated tag. **No "v" prefix.** Use the exact `TAG_VERSION` from Step 1: + +```bash +git tag -a "$TAG_VERSION" -m "$DESCRIPTION" +``` + +Verify the tag exists: + +```bash +git tag -l "$TAG_VERSION" +``` + +If the output is empty, the tag creation failed. Report the error and stop. + +## Step 8: Push the Tag + +**Check that remote exists before pushing:** +```bash +git remote get-url origin +``` +If this fails, the remote is not configured. Report the error and stop. + +Push to the remote: + +```bash +git push origin "$TAG_VERSION" +``` + +**Check exit code:** If the push returns a non-zero exit code, report the error and stop. Common failures: +- **Remote not configured:** Suggest adding a remote (`git remote add origin <url>`) +- **Permission denied:** Check SSH keys or token permissions +- **Network error:** Retry once, then report + +**Clean up temporary branch if we were in detached HEAD:** +```bash +if [ "$WAS_DETACHED" = true ]; then + TEMP_BRANCH=$(git branch --show-current) + git checkout --detach 2>/dev/null + git branch -D "$TEMP_BRANCH" 2>/dev/null || true +fi +``` + +## Step 9: Verify the Push + +Confirm the tag is on the remote: + +```bash +git ls-remote --tags origin | grep "$TAG_VERSION" +``` + +If this returns nothing, the push failed. Report the error and stop. + +## Step 10: Report + +Print a final summary using actual variable values: + +``` +Tagged: $TAG_VERSION +From: ${PREV_TAG:-initial} +Commits: $(git log --oneline "${PREV_TAG:-HEAD}..HEAD" | wc -l) +Pushed: origin/$TAG_VERSION + +$DESCRIPTION +``` + +## Gotchas + +- **No "v" prefix.** The tag must use the exact version string from the package manager file (e.g., `1.3.5`, not `v1.3.5`). +- **Tags are immutable.** Once pushed, a tag cannot be overwritten. The skill checks for existing tags locally and on the remote before creating. +- **Clean working tree required.** The skill stops if there are uncommitted changes. A dirty tree means the tag would not represent a clean release. +- **Detached HEAD is handled automatically.** If in detached HEAD state, the skill creates a temporary branch for tagging. + +--- diff --git a/.skills/purge-branches/SKILL.md b/.skills/purge-branches/SKILL.md new file mode 100644 index 000000000..0ca66be57 --- /dev/null +++ b/.skills/purge-branches/SKILL.md @@ -0,0 +1,106 @@ +--- +name: purge-branches +description: Deletes all local git branches except 'main'. Works in any git repository — no npm or package.json dependency. +license: BSD 3-Clause +compatibility: Requires git CLI. Must be run from a git repository root. +metadata: + agent: coding +--- + +# Purge Branches + +> **⚠️ EXECUTION RULE:** Every code block in this skill is a shell command to **execute**. Do not print them as text, explain them, or treat them as examples — run them directly. + +Delete all local branches except `main`. Pure git — no npm, no package.json required. + +## Pre-flight Check + +Check current state and confirm we're not on a branch that will be deleted: + +```bash +# Check current branch +CURRENT_BRANCH=$(git branch --show-current) + +# Handle detached HEAD +if [ -z "$CURRENT_BRANCH" ]; then + echo "ERROR: You are in detached HEAD state. Switch to a branch before purging." + echo " git checkout main" + exit 1 +fi + +echo "Current branch: $CURRENT_BRANCH" + +# Warn if not on main — current branch cannot be deleted +if [ "$CURRENT_BRANCH" != "main" ]; then + echo "WARNING: You are on '$CURRENT_BRANCH', not 'main'. The current branch will be skipped." +fi + +# Show branches that will be deleted +echo "" +echo "Branches that will be deleted:" +git branch --format='%(refname:short)' | grep -v '^main$' | grep -v "^${CURRENT_BRANCH}$" +BRANCH_COUNT=$(git branch --format='%(refname:short)' | grep -v '^main$' | grep -vc "^${CURRENT_BRANCH}$") +echo "" +echo "Total: $BRANCH_COUNT branches to delete" + +if [ "$BRANCH_COUNT" -eq 0 ]; then + echo "No branches to purge." + exit 0 +fi +``` + +## Execute + +Delete all branches except `main`: + +```bash +# Delete all branches except main and current (safely, one at a time) +git branch --format='%(refname:short)' | grep -v '^main$' | grep -v "^${CURRENT_BRANCH}$" | while read -r branch; do + if git branch -D "$branch" 2>&1; then + echo "Deleted: $branch" + else + echo "Skipped: $branch (delete failed)" + fi +done +``` + +**Handle failure:** If any branch fails to delete (e.g., it's checked out), the loop continues — only successfully deleted branches are counted. Report any failures at the end. + +## Report + +```bash +# Count remaining branches (excluding main and current) +REMAINING=$(git branch --format='%(refname:short)' | grep -v '^main$' | grep -vc "^${CURRENT_BRANCH}$") +echo "" +echo "Purge complete." +echo "Remaining branches: $REMAINING" +``` + +## Examples + +``` +User: purge-branches + +Agent: Checking current branch: feature/add-auth +Branches that will be deleted: +feature/add-auth +fix/typo-in-readme +chore/update-deps + +Total: 3 branches to delete + +Deleted: feature/add-auth +Deleted: fix/typo-in-readme +Deleted: chore/update-deps + +Purge complete. +Remaining branches: 0 +``` + +## Gotchas + +- **The current branch is safe.** The skill only deletes branches other than `main`. If you're on `main`, no branches are deleted. +- **Force delete (`-D`) is used.** Branches that haven't been merged are deleted without checking merge status. Verify you don't need a branch before running. +- **Cannot undo.** Once deleted, local branches are gone. Use `git reflog` to recover if needed, but this is not guaranteed. + +--- diff --git a/.skills/restructure-code/SKILL.md b/.skills/restructure-code/SKILL.md new file mode 100644 index 000000000..cb5f8fd80 --- /dev/null +++ b/.skills/restructure-code/SKILL.md @@ -0,0 +1,373 @@ +--- +name: restructure-code +description: "Audits each directory in ./src for opportunities to restructure directories to align with community best practices & better organization in general. Generates one consolidated issue per directory via /create-issue." +license: BSD 3-Clause +compatibility: Node.js project with ./src directory structure +metadata: + version: 1.0.0 + phase-mode: strict-sequential + target: ./src + agent: code-review +--- + +# Restructure Code + +> **⚠️ EXECUTION RULE:** Every code block in this skill is a shell command to **execute**. Do not print them as text, explain them, or treat them as examples — run them directly. + +Audit the `./src` directory tree for opportunities to restructure directories to align with community best practices and better organization. Execute in **strict sequential phases** — one directory per phase, no lookahead, no batching. + +## State Management + +Use a state file to persist progress across responses. The state file is cleaned at the start of a new audit and at the end when all phases are complete. + +### State File Location + +An example name like `restructure-state.md` — the agent decides where to place it. + +Store the path in a variable: +```bash +STATE_FILE="${STATE_FILE_PATH:-restructure-state.md}" +``` + +### State File Format + +```markdown +# Restructure State + +## Phase Queue +- [x] ./src +- [ ] ./src/components +- [ ] ./src/utils + +## Current Phase +./src/components + +## Completed +- ./src + +## Findings +[Current phase findings stored here as markdown table] +``` + +### State Lifecycle + +1. **Start of audit** — Delete any existing state file. Begin fresh. +2. **After each phase** — Save the current state (completed directories, current phase, findings). +3. **On resume** — Read the state file to determine where to pick up. +4. **End of audit** — Delete the state file once all phases are complete. + +--- + +## Phase Protocol + +### Phase 0: Discovery + +1. **Clean state.** Delete any existing state file: `rm -f "$STATE_FILE"` +2. **Enumerate directories.** List all immediate subdirectories of `./src`: + ```bash + find ./src -mindepth 1 -maxdepth 1 -type d | sort + ``` +3. Sort them alphabetically (the `find | sort` above handles this). +4. The root `./src` itself is Phase 1. +5. Build the phase queue: `[./src, subdir1, subdir2, ...]` in alphabetical order. +6. **Save the phase queue** to the state file: + ```bash + cat > "$STATE_FILE" << EOF + # Restructure State + + ## Phase Queue + - [x] ./src + EOF + find ./src -mindepth 1 -maxdepth 1 -type d | sort | while read dir; do + echo "- [ ] $dir" >> "$STATE_FILE" + done + echo "" >> "$STATE_FILE" + echo "## Current Phase" >> "$STATE_FILE" + echo "./src" >> "$STATE_FILE" + echo "" >> "$STATE_FILE" + echo "## Completed" >> "$STATE_FILE" + echo "- ./src" >> "$STATE_FILE" + echo "" >> "$STATE_FILE" + echo "## Findings" >> "$STATE_FILE" + ``` +7. **Proceed immediately** to Phase 1 (the first directory in the queue). Do not wait for user confirmation. + +### Per-Phase Workflow (repeat for each directory) + +#### Step 0: Resume Check + +Before beginning a phase, check for existing state: + +1. Read An example name like `restructure-state.md` — the agent decides where to place it.. +2. If the file exists and the current phase matches the directory being processed, continue. +3. If the file exists and a different phase is listed as current, the audit was interrupted — resume from the saved state. +4. If the file does not exist, begin fresh. + +#### Step A: Enumerate Files + +Determine the scope based on the current phase: +- **If the phase is `./src` (root):** List only files directly in the `./src` directory (non-recursive). +- **If the phase is a subdirectory:** List all source files recursively within that subdirectory only. + +Exclude: +- `node_modules/` +- `__tests__/` or `*.test.*` / `*.spec.*` +- Generated files (`.js.map`) — **Note:** `.d.ts` files are NOT excluded; they provide valuable type information for understanding structure +- Config files (`tsconfig.json`, `.eslintrc`, etc.) +- Static assets (`.png`, `.jpg`, `.svg`, `.css`) + +Acceptable targets: `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`, `.d.ts`. + +**Handle empty directories:** If no source files are found after filtering, log "No source files found in [directory]. Skipping analysis." and move to the next phase. Do not create an issue for empty directories. + +#### Step B: Analyze Structure + +For each directory, perform a **sequential** analysis for restructuring opportunities: + +1. **Cohesion** — Are related files scattered? Do files in this directory share a common purpose, or is it a grab-bag of unrelated concerns? Look for: + - Files that logically belong together but are split across directories + - Directories that mix multiple concerns (e.g., UI components mixed with business logic) + - Orphaned files with no clear parent category + +2. **Coupling** — Are there cross-directory dependencies that suggest consolidation? Look for: + - Files in this directory that import heavily from a single other directory + - Bidirectional imports between two directories (suggests they should be one) + - Shared utilities duplicated across multiple directories + +3. **Naming & Convention** — Does the directory follow established patterns? Look for: + - Inconsistent naming (e.g., `utils` vs `helpers`, `components` vs `ui`) + - Directories that don't match common architectural patterns (feature-based, layer-based, domain-driven) + - Mixed conventions within a single directory (e.g., some files use PascalCase, others camelCase for grouping) + +4. **Depth & Nesting** — Is the directory tree too deep or too flat? Look for: + - Nesting deeper than 3-4 levels (hard to navigate, long import paths) + - Directories with only 1-2 files that could be merged with siblings + - Flat directories with 20+ files that should be subdivided + +5. **Pattern Alignment** — Does the structure align with community best practices? Look for: + - Opportunities to adopt feature-based organization (group by feature, not by type) + - Opportunities to adopt layer-based organization (separate UI, business logic, data access) + - Opportunities to adopt domain-driven boundaries + - Mixed organizational patterns within the same project + +For each finding, record: +- **File/Directory** — relative path from project root +- **Issue Type** — `cohesion`, `coupling`, `naming`, `depth`, or `pattern` +- **Impact** — `high`, `medium`, or `low` +- **Summary** — one-line description of the restructuring opportunity +- **Recommendation** — brief suggested action + +#### Step C: Consolidate Findings + +After analyzing all files in the directory: + +- **If zero issues found:** Log "No restructuring opportunities detected in [directory]." and move to the next phase. +- **If issues found:** Proceed to Step D. + +#### Step D: Create Issue + +Invoke the `create-issue` skill as a chain instruction (text delegation). The issue body will be replaced in Step D2. + +The title should be concise and descriptive, prefixed with `refactor:` since restructuring is a refactor. Example: `refactor: restructure src/components for better cohesion` + +**Capture the issue number** from the `create-issue` output. The `create-issue` skill prints `ISSUE_NUMBER=<number>` as structured output. Read it from the conversation history: + +```bash +# Capture ISSUE_NUMBER from the conversation history (last occurrence wins) +# The chained skill prints lines like: ISSUE_NUMBER=42 +ISSUE_NUMBER=$(echo "$CONVERSATION_HISTORY" | sed -n 's/^ISSUE_NUMBER=\([0-9]\{1,\}\)$/\1/p' | tail -1) +if [ -z "$ISSUE_NUMBER" ]; then + echo "ERROR: Could not capture ISSUE_NUMBER from create-issue output. Conversation history:" + echo "$CONVERSATION_HISTORY" | tail -20 + exit 1 +fi +echo "ISSUE_NUMBER=$ISSUE_NUMBER" +``` + +**If create-issue fails:** Log the error, skip Step D2 (body update), and proceed to Step E. Do not abort the phase. + +**After creating the issue (or failing to), continue to Step D2.** Do not stop or wait for further input — the pipeline proceeds automatically. + +##### Step D2: Replace Issue Body + +Once the issue is created, replace the dummy body with the full restructuring analysis. + +**Write the analysis to a temp file** to avoid echo escaping issues with multi-line content and backticks: + +**Cleanup:** Remove `BODY_FILE` once the issue is created, or when cleaning up after a failure. Do not rely on a shell `trap` — delete the temp file explicitly when the task completes. + +```bash +BODY_FILE=$(mktemp) + +# Build the full analysis body — use heredoc to preserve formatting +cat > "$BODY_FILE" << BODYEOF +## Restructure Analysis: $DIRECTORY_NAME + +### Summary +$SUMMARY_TEXT + +### Findings + +| File/Directory | Issue Type | Impact | Summary | Recommendation | +|---------------|-----------|--------|---------|----------------| +$FINDINGS_TABLE + +### Proposed Structure + +\`\`\` +$PROPOSED_STRUCTURE +\`\`\` + +### Restructuring Priority +1. $HIGH_PRIORITY_ITEMS +2. $MEDIUM_PRIORITY_ITEMS +3. $LOW_PRIORITY_ITEMS +BODYEOF + +# Update the issue — use dynamic repo variable derived from git remote +GIT_REMOTE=$(git remote get-url origin 2>/dev/null) +if echo "$GIT_REMOTE" | grep -q 'github\.com'; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\)\.git$/\1/') + if [ -z "$REPO" ]; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\)$/\1/') + fi + GH_REPO_OPT="--repo $REPO" +else + echo "WARNING: Could not parse GitHub repository from remote. Skipping issue body update." + GH_REPO_OPT="" +fi + +if [ "$ISSUE_NUMBER" != "unknown" ]; then + gh issue edit "$ISSUE_NUMBER" --body-file "$BODY_FILE" $GH_REPO_OPT 2>&1 + if [ $? -ne 0 ]; then + echo "WARNING: Failed to update issue body. Issue $ISSUE_NUMBER exists but body was not updated." + else + # Verify the body was updated successfully + VERIFY_BODY=$(gh issue view "$ISSUE_NUMBER" --json body --jq '.body' $GH_REPO_OPT 2>/dev/null || true) + if ! echo "$VERIFY_BODY" | grep -q "Restructure Analysis: $DIRECTORY_NAME"; then + echo "WARNING: Failed to verify restructure analysis was written to issue #$ISSUE_NUMBER." + else + echo "Restructure analysis verified on issue #$ISSUE_NUMBER." + fi + fi +fi + +rm -f "$BODY_FILE" +``` + +**Key variable replacements:** +- `$DIRECTORY_NAME` — the directory being analyzed (e.g., `src/components`) +- `$SUMMARY_TEXT` — brief summary of findings +- `$FINDINGS_TABLE` — markdown table rows (one per finding) +- `$PROPOSED_STRUCTURE` — proposed directory tree +- `$HIGH_PRIORITY_ITEMS`, `$MEDIUM_PRIORITY_ITEMS`, `$LOW_PRIORITY_ITEMS` — prioritized lists + +**Never use `echo` for multi-line content** — always use heredoc (`cat > file << EOF`) to avoid escaping issues with backticks, special characters, and newlines. + +#### Step E: Update State + +After completing the phase (issue created or no issues found), update the state file: + +1. **Mark the current directory as completed** in the phase queue: + ```bash + # Use | as sed delimiter instead of / to handle directory paths safely + ESCAPED_DIR=$(echo "$CURRENT_DIR" | sed 's/[|/]/\\&/g') + sed "s|- \[ \] $ESCAPED_DIR|- [x] $ESCAPED_DIR|" "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" + ``` +2. **Append to Completed section:** + ```bash + sed '/^## Completed$/a - '"$CURRENT_DIR" "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" + ``` +3. **Update Current Phase** to the next directory in the queue, or clear it if this was the last: + ```bash + NEXT_DIR=$(grep -A100 "## Phase Queue" "$STATE_FILE" | grep "\- \[ \]" | head -1 | sed 's/- \[ \] //') + if [ -n "$NEXT_DIR" ]; then + sed "s/^## Current Phase$/## Current Phase\n$NEXT_DIR/" "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" + else + sed '/^## Current Phase$/d' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" + fi + ``` +4. **Save the updated state** to `$STATE_FILE` (the sed commands above modify in place). + +Example state after completing `./src`: + +```markdown +# Restructure State + +## Phase Queue +- [x] ./src +- [x] ./src/components +- [ ] ./src/utils + +## Current Phase +./src/utils + +## Completed +- ./src +- ./src/components + +## Findings +[Next phase findings will be stored here] +``` + +#### Step E2: Check Completion + +After updating state, check if all phases are complete: + +- **If all directories are marked `[x]`:** Delete the state file (`rm -f "$STATE_FILE"`) and report "All phases complete." +- **If more directories remain:** Automatically proceed to the next phase in the queue. + +#### Step E3: Phase Complete + +- Log completion of the current directory. +- **Auto-advance** to the next phase immediately. +- Do NOT wait for user confirmation unless the audit is fully complete. + +## Execution Rules + +1. **ONE PHASE PER RESPONSE.** Each response processes exactly one directory. The skill will **auto-advance** to the next phase immediately upon completion, requiring no manual prompts between phases. +2. **STRICT ORDERING.** Process directories in alphabetical order. Parent before child. +3. **NO LOOKAHEAD.** Do not read files from the next directory until the current phase is complete. +4. **NO BATCHING.** Do not combine multiple directories into a single issue or response. +5. **SEQUENTIAL FILE ANALYSIS.** Analyze files one at a time within a directory. +6. **STOP CONDITION.** Terminate when all directories in the phase queue have been processed. +7. **STATE PERSISTENCE.** Always save state after each phase. Always read state before starting a phase. + +## Impact Guidelines + +| Impact | Criteria | +|--------|----------| +| **High** | Major cohesion/coupling issues, mixed concerns, structural anti-patterns that significantly impact maintainability | +| **Medium** | Naming inconsistencies, pattern misalignment, moderate nesting issues, duplicated utilities | +| **Low** | Minor organizational tweaks, naming conventions, cosmetic restructuring | + +**Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to classify each finding's impact. Issue one `choice` question per finding, passing the finding as state. + +``` +Question: "What is the impact of this restructuring opportunity?" +Type: choice +Options: high, medium, low +Criteria: + high = major cohesion/coupling issues, mixed concerns, structural anti-patterns + medium = naming inconsistencies, pattern misalignment, moderate nesting, duplicated utilities + low = minor organizational tweaks, naming conventions, cosmetic restructuring +Example answer: medium +``` + +Interpret the returned label directly. Fall back to the impact guidelines above if no decision tool is available. + +## Output Format + +Each phase response must include: + +``` +## Restructure: [directory path] +- **Status:** completed | blocked | no-issues +- **Files Analyzed:** [count] +- **Opportunities Found:** [count] (high: N, medium: N, low: N) +- **Details:** + - [key-finding-1] + - [key-finding-2] +- **Action Items:** [issue created with ID or "No restructuring opportunities detected"] +- **Next Steps:** [next directory name or "All phases complete"] +``` \ No newline at end of file diff --git a/.skills/scan-issues/SKILL.md b/.skills/scan-issues/SKILL.md new file mode 100644 index 000000000..0fb1971d8 --- /dev/null +++ b/.skills/scan-issues/SKILL.md @@ -0,0 +1,235 @@ +--- +name: scan-issues +description: Scans GitHub for open issues labeled 'approved' (excluding those also labeled 'in progress'), processes them via fix-issue, opens PRs with auto-merge, and resolves merge conflicts. Runs hourly. +license: BSD 3-Clause +compatibility: Requires gh CLI authenticated with a GitHub repo. Must be run from a git project root. +metadata: + agent: coding +--- + +# Scan & Fix Issues + +> **⚠️ EXECUTION RULE:** Every code block in this skill is a shell command to **execute**. Do not print them as text, explain them, or treat them as examples — run them directly. + +An autonomous issue scanner and fixer. Finds approved, unassigned issues and processes them through the `fix-issue` pipeline. Worktree isolation is handled internally by `create-feature`. + +## Workflow + +### 1. Determine Repository + +Capture the project root and extract the repo from the git remote — never hardcode paths: + +```bash +PROJECT_ROOT=$(git rev-parse --show-toplevel) +GIT_REMOTE=$(git remote get-url origin 2>/dev/null) +if [ -z "$GIT_REMOTE" ]; then + echo "ERROR: No git remote 'origin' found. Can't determine repository." + exit 1 +fi + +if echo "$GIT_REMOTE" | grep -q '^git@'; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\)\.git$/\1/') + if [ -z "$REPO" ]; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\)$/\1/') + fi +elif echo "$GIT_REMOTE" | grep -q 'github\.com'; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\)\.git$/\1/') + if [ -z "$REPO" ]; then + REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\)$/\1/') + fi +else + echo "ERROR: Could not parse repository from remote '$GIT_REMOTE'." + exit 1 +fi + +# Validate REPO is non-empty and contains a slash (owner/repo format) +if [ -z "$REPO" ] || ! echo "$REPO" | grep -q '/'; then + echo "ERROR: Parsed repository '$REPO' does not look like 'owner/repo'. Check remote URL." + exit 1 +fi +``` + +### 2. Scan for Issues + +```bash +gh issue list --state open --label approved --search '-label:"in progress"' --json number,title,url,labels --repo "$REPO" +``` + +This pushes the filtering to the GitHub API — only issues with `approved` but **not** `in progress` are returned. No client-side `jq` filtering needed. + +If no issues match, report `No approved, unassigned issues found.` and exit cleanly. + +### 3. Parse Results + +Extract each issue's `number`, `title`, `url`, and `labels` from the JSON array. + +### 4. Process Issues + +**Process each issue independently.** Issues can be processed in parallel — spawn subagents or dispatch concurrent `fix-issue` runs. Worktree isolation is handled internally by `create-feature`, so parallel runs do not conflict: + +For each issue: + +1. **Invoke fix-issue** as a chain instruction. + This chains through the full `fix-issue` → `create-feature` pipeline. The `create-feature` skill handles worktree creation and cleanup internally. + +2. **Wait for completion.** Each `fix-issue` run completes on its own; parallel runs proceed independently. + +3. **Extract PR_NUMBER from fix-issue output.** The `fix-issue` chain prints `PR_NUMBER=<number>` as structured output in the conversation history. Read it from there — do not attempt to grep a shell variable. + + Add a reusable capture pattern to extract `PR_NUMBER` from the conversation history after `fix-issue` completes: + + ```bash + # Capture PR_NUMBER from the conversation history (last occurrence wins) + PR_NUMBER=$(echo "$CONVERSATION_HISTORY" | sed -n 's/^PR_NUMBER=\([0-9]\{1,\}\)$/\1/p' | tail -1) + if [ -z "$PR_NUMBER" ]; then + echo "WARNING: Could not capture PR_NUMBER from fix-issue output. Issue may have been skipped." + else + echo "PR_NUMBER=$PR_NUMBER" + fi + ``` + + If `PR_NUMBER` is empty, the issue was skipped or the pipeline didn't create a PR — log it and continue. + +4. **If fix-issue succeeds:** Log the issue number and PR number. + +5. **If fix-issue fails:** Log the error, skip the issue, and continue with the next. Never abort the queue over a single failure. + +**Rate limiting:** If GitHub API returns 403 rate limit, wait 60 seconds and retry the current issue once. + +**Timeout handling:** Each fix-issue run can take 30-60 minutes (via create-feature). If a run exceeds 60 minutes, report a timeout warning but do not abort — the issue may still be processing. + +### 6. PR Auto-Merge + +The `create-feature` → `commit-push` pipeline within `fix-issue` handles PR creation. After extracting `PR_NUMBER` from the fix-issue output: + +- Set it to auto-merge if the repo supports it: + ```bash + gh pr merge "$PR_NUMBER" --auto --merge --repo "$REPO" + + # Verify auto-merge was enabled successfully + AUTO_MERGE=$(gh pr view "$PR_NUMBER" --json autoMerge --jq '.enabled' --repo "$REPO" 2>/dev/null || true) + if [ "$AUTO_MERGE" != "true" ]; then + echo "WARNING: Auto-merge may not be enabled for PR #$PR_NUMBER (state: $AUTO_MERGE)." + else + echo "Auto-merge verified for PR #$PR_NUMBER." + fi + ``` +- If auto-merge is not available or fails, note it in the report but do not block on it. + +### 7. Resolve Merge Conflicts + +If a PR cannot merge because it conflicts with the default branch, resolve the conflict in an **isolated worktree** so parallel runs do not collide on the shared checkout. Each conflict resolution uses its own session ID and worktree path (same pattern as `create-feature`). + +1. **Detect the conflict.** Check the PR's mergeable state: + ```bash + MERGEABLE=$(gh pr view "$PR_NUMBER" --json mergeable --jq '.mergeable' --repo "$REPO") + if [ "$MERGEABLE" = "CONFLICTING" ]; then + echo "PR #$PR_NUMBER has merge conflicts. Resolving..." + fi + ``` + +2. **Create an isolated worktree for the PR branch.** Use a unique session ID and a sibling directory so parallel runs never share a checkout: + ```bash + PROJECT_ROOT=$(git rev-parse --show-toplevel) + WORKTREES_DIR="$(dirname "$PROJECT_ROOT")/$(basename "$PROJECT_ROOT").worktrees" + mkdir -p "$WORKTREES_DIR" + SESSION_ID=$(head -c 8 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' || echo $RANDOM$RANDOM$RANDOM) + WORKTREE_PATH="${WORKTREES_DIR}/conflict-${SESSION_ID}" + DEFAULT_BRANCH=$(git remote show origin | sed -n 's/.*HEAD branch: //p') + PR_BRANCH=$(gh pr view "$PR_NUMBER" --json headRefName --jq '.headRefName' --repo "$REPO") + + git fetch origin "$PR_BRANCH" "$DEFAULT_BRANCH" + git worktree add "$WORKTREE_PATH" "origin/$PR_BRANCH" + cd "$WORKTREE_PATH" + ``` + +3. **Merge the default branch into the PR branch.** Use `--no-commit` so the merge is staged but not committed, letting the same commit step handle both the clean and conflicted cases: + ```bash + git merge --no-commit "origin/$DEFAULT_BRANCH" + ``` + +4. **Resolve conflicts.** If the merge reports conflicts, resolve them manually: + - Inspect each conflicted file: `git status` lists them. + - Edit each file to keep the correct changes, removing conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`). + - Stage the resolved files: `git add <file>`. + +5. **Commit and push.** Once the merge is complete (clean or resolved) and staged: + ```bash + git commit --no-edit + git push origin "HEAD:refs/heads/$PR_BRANCH" + ``` + +6. **Verify.** Confirm the PR is now mergeable: + ```bash + gh pr view "$PR_NUMBER" --json mergeable --jq '.mergeable' --repo "$REPO" + ``` + If it reports `MERGEABLE`, the PR can merge. If it still reports `CONFLICTING`, repeat the resolution steps or report the unresolved conflict. + +7. **Clean up the worktree.** Return to the original directory and remove the worktree: + ```bash + cd "$PROJECT_ROOT" + git worktree remove "$WORKTREE_PATH" --force + ``` + +**Note:** Each conflict resolution runs in its own worktree with its own session ID, so parallel runs never share a checkout and never collide on the same branch. If the PR branch is already up to date with the default branch (no merge needed), `git merge --no-commit` reports "Already up to date" — skip the commit and push, then clean up. + +### 8. Report + +After all issues are processed, produce a structured summary: + +``` +Scan complete. +✅ Fixed: #42 — "Crash on empty input" (PR #101, auto-merged) +✅ Fixed: #57 — "Missing error boundary" (PR #102, auto-merged) +❌ Skipped: #89 — "Auth timeout race" (fix-issue failed) + +Total scanned: 3 +Success: 2 +Failed: 1 +``` + +## Error Handling + +- **No `gh` auth:** Suggest `gh auth login`. +- **API rate limit:** Wait 60 seconds, retry once. If it fails again, report the rate limit and stop. +- **Network error:** Retry once, then report. +- **fix-issue failure on individual issue:** Log it, skip it, continue with the next. Never abort the entire queue over a single failure. + +## Cron Schedule + +This skill is designed to run hourly. When triggered via cron, execute the full workflow above without requiring user input. + +## Examples + +``` +User: scan-issues + +Agent: Determining repository... avoidwork/madz +Scanning for approved issues... +Found 3 approved, unassigned issues. + +Processing issue #42 — "Crash on empty input"... + Invoking fix-issue... + PR created: #101 + Auto-merge: ENABLED + +Processing issue #57 — "Missing error boundary"... + Invoking fix-issue... + PR created: #102 + Auto-merge: ENABLED + +Processing issue #89 — "Auth timeout race"... + Invoking fix-issue... + fix-issue failed: gh auth not configured + +Scan complete. +✅ Fixed: #42 — "Crash on empty input" (PR #101, auto-merged) +✅ Fixed: #57 — "Missing error boundary" (PR #102, auto-merged) +❌ Skipped: #89 — "Auth timeout race" (fix-issue failed) + +Total scanned: 3 +Success: 2 +Failed: 1 +``` + +--- diff --git a/.skills/update-pr/SKILL.md b/.skills/update-pr/SKILL.md new file mode 100644 index 000000000..8af008149 --- /dev/null +++ b/.skills/update-pr/SKILL.md @@ -0,0 +1,242 @@ +--- +name: update-pr +description: Update an existing PR's title and description by scanning project rules for conventions and the PR template, synthesizing both from the delta between the branch and target, then applying changes via 'gh api' (gh pr edit fails in this repo). +license: BSD 3-Clause +compatibility: Requires gh CLI authenticated with a repo that has .github/PULL_REQUEST_TEMPLATE.md. +metadata: + version: 1.0 + agent: coding +--- + +# Update PR + +> **⚠️ EXECUTION RULE:** Every code block in this skill is a shell command to **execute**. Do not print them as text, explain them, or treat them as examples — run them directly. + +Update an existing pull request's title and description following project conventions. + +## When to Use + +- The user asks to update a PR's title or description +- A PR was opened with a generic or incomplete title/description +- The user wants to align PR metadata with project rules + +## Steps + +1. **Identify the PR** + + The PR number is provided in the chain context (the text after `/update-pr`). If no PR number is provided, report an error and stop — this skill requires a specific PR to update. + + **Determine the target repository dynamically from the git remote.** Never hardcode a repo name: + + ```bash + # Extract owner/repo from git remote URL — handles both HTTPS and SSH + GIT_REMOTE=$(git remote get-url origin 2>/dev/null) + if echo "$GIT_REMOTE" | grep -q '^git@'; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\)\.git$/\1/') + if [ -z "$GH_REPO" ]; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\)$/\1/') + fi + elif echo "$GIT_REMOTE" | grep -q 'github\.com'; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\)\.git$/\1/') + if [ -z "$GH_REPO" ]; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\)$/\1/') + fi + else + echo "ERROR: Could not parse repository from remote '$GIT_REMOTE'." + exit 1 + fi + + # Validate GH_REPO is non-empty and contains a slash (owner/repo format) + if [ -z "$GH_REPO" ] || ! echo "$GH_REPO" | grep -q '/'; then + echo "ERROR: Parsed repository '$GH_REPO' does not look like 'owner/repo'. Check remote URL." + exit 1 + fi + ``` + + Confirm the PR exists: + ```bash + gh pr view <PR_NUMBER> --json title,state,url --repo "$GH_REPO" + ``` + +2. **Scan Project Rules** + + Scan the project root and any specified paths for a `.agent` rules file and read its contents: + + ``` + # Scan the project root for AGENTS.md or similar rules file + ``` + + Extract: + - **Commit style** (Conventional Commits format from section 5.1) + - **PR template** requirements from section 5.4 (and `.github/PULL_REQUEST_TEMPLATE.md` if it exists) + - Any project-specific rules that affect the PR description + +3. **Read the PR template** + + ```bash + if [ -f ".github/PULL_REQUEST_TEMPLATE.md" ]; then + cat .github/PULL_REQUEST_TEMPLATE.md + else + echo "WARNING: No .github/PULL_REQUEST_TEMPLATE.md found. Using default structure." + fi + ``` + + If the file doesn't exist, construct a reasonable description with: Description, Type of Change, Testing, Coverage, Checklist. + +4. **Gather the full delta** + + You must collect the complete picture — every commit and every file change between the branch and its target. Do not rely on the most recent commit alone. + + ```bash + # Get base and head refs from the PR + BASE_REF=$(gh pr view <PR_NUMBER> --json baseRefName --jq '.baseRefName' --repo "$GH_REPO") + HEAD_REF=$(gh pr view <PR_NUMBER> --json headRefName --jq '.headRefName' --repo "$GH_REPO") + ``` + + From this data, identify: + - **All files changed** and what kind of change each represents + - **All commits** and their messages (these are your primary signal for the title) + - **The nature of changes**: code, docs, tests, config, etc. + + ```bash + # Get all commits on the branch (oldest first) + git log --reverse "${BASE_REF}..${HEAD_REF}" --oneline --no-decorate + + # Get the full diff stats (files changed, lines added/removed) + git diff "${BASE_REF}..${HEAD_REF}" --stat + + # Get the full diff (for detailed change analysis) — NOTE: On large branches, this may produce very large output. + # If output exceeds ~50,000 characters, use --stat only and note "Large delta — full diff truncated." + git diff "${BASE_REF}..${HEAD_REF}" + ``` + +5. **Draft the title** + + Synthesize the title from the **complete delta** — all commits and all file changes. Follow Conventional Commits format: `<type>: <short description>` + + **Type selection priority** (use the highest-priority type present in the delta): + 1. `fix:` — if any commit fixes a bug + 2. `feat:` — if any commit adds a feature + 3. `docs:` — if only documentation/spec changes + 4. `test:` — if only test changes + 5. `refactor:` — if only refactoring with no behavior change + 6. `chore:` — if only maintenance tasks + + **Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to pick the type instead of reasoning it out manually. Feed the commit delta as state. + + ``` + Question: "Which conventional-commit type should the PR title use?" + Type: choice + Options: fix, feat, docs, test, refactor, chore + Criteria: + fix = any commit fixes a bug + feat = any commit adds a feature + docs = only documentation/spec changes + test = only test changes + refactor = only refactoring with no behavior change + chore = only maintenance tasks + Example answer: feat + ``` + + Interpret the returned label directly. Fall back to the priority rule above if no decision tool is available. + + **Description selection:** + - If all commits share a common theme, use that as the description. + - If commits span multiple themes, use the **primary** theme (the one with the most commits or the most impactful change). + - Keep it under 72 characters. Be specific — avoid vague phrases like "update" or "changes." + + Examples: + - `fix: resolve agent stalling on empty conversation history` + - `feat: add cron-based auto-reflection scheduling` + - `docs: add compliance framework and borderline escalation rules to safety guidelines` + + **Assign the synthesized title:** + ```bash + DRAFTED_TITLE="<type>: <short description>" + echo "DRAFTED_TITLE=$DRAFTED_TITLE" + ``` + +6. **Draft the description** + + Synthesize the description from the **complete delta** — all commits and all file changes. Fill out every section of the PR template. Never leave a section blank — use `N/A` if not applicable. + + **Description body:** + - Write a concise 1-3 sentence summary of what changed across the **entire branch**, not just the last commit. + - Reference all files changed and the nature of each change. + - If the branch has multiple commits, mention the scope: e.g., "This change adds X and also updates Y to align with Z." + + **Template sections:** + - **Type of Change:** Check the box that matches the highest-priority type (same priority as title: fix > feat > docs > test > refactor > chore). If multiple types apply, check the highest-priority one. + - **Testing:** Describe tests written/run and their coverage. Use "N/A" for doc-only or config-only changes. + - **Coverage:** Confirm 100% line coverage maintained (check the box if no code changes or if coverage is maintained). + - **Checklist:** Check all boxes if no violations. Uncheck any that don't apply. + +7. **Apply via `gh api`** + + **Never use `gh pr edit` — it fails in this repo.** Use the GitHub API instead. Continue using the `$GH_REPO` variable extracted in Step 1: + + ```bash + # Build JSON payload with jq to handle multi-line body safely + # If jq is unavailable, fall back to temp file approach + if command -v jq >/dev/null 2>&1; then + PAYLOAD=$(jq -n --arg title "$DRAFTED_TITLE" --arg body "$DRAFTED_BODY" '{title: $title, body: $body}') + if ! echo "$PAYLOAD" | gh api "repos/$GH_REPO/pulls/$PR_NUMBER" --input - >/dev/null 2>&1; then + echo "ERROR: Failed to update PR #$PR_NUMBER via API." + exit 1 + fi + else + # Fallback: write body to temp file + PR_BODY_FILE=$(mktemp) + printf '%s\n' "$DRAFTED_BODY" > "$PR_BODY_FILE" + if ! gh api "repos/$GH_REPO/pulls/$PR_NUMBER" \ + -f title="$DRAFTED_TITLE" \ + -f body="$(cat "$PR_BODY_FILE")" >/dev/null 2>&1; then + echo "ERROR: Failed to update PR #$PR_NUMBER via API." + rm -f "$PR_BODY_FILE" + exit 1 + fi + rm -f "$PR_BODY_FILE" + fi + + # Verify the update succeeded by reading back the title + VERIFY_TITLE=$(gh pr view "$PR_NUMBER" --json title --jq '.title' --repo "$GH_REPO" 2>/dev/null || true) + if [ "$VERIFY_TITLE" != "$DRAFTED_TITLE" ]; then + echo "ERROR: PR title update verification failed. Expected '$DRAFTED_TITLE', got '$VERIFY_TITLE'." + exit 1 + fi + echo "PR title verified: $VERIFY_TITLE" + ``` + + **Handle API errors:** If the API call returns a non-200 status, report the error and stop. Common errors: + - **403 Forbidden:** Check authentication and permissions + - **404 Not Found:** Verify PR number and repo + - **422 Unprocessable Entity:** Check title/body format (title under 72 chars, body not empty) + + **Coverage verification:** The skill instructs to "Confirm 100% line coverage maintained" but there's no way to verify this without running tests. If code changes were made, run `npm run coverage` and check the output. If no code changes were made (docs-only, config-only), note "No code changes — coverage N/A." + +8. **Confirm** + + Show the user the updated PR URL and a summary of what changed. + +## Example + +```bash +gh api "repos/$GH_REPO/pulls/<PR_NUMBER>" \ + -f title="feat: add OpenAPI spec generation for REST endpoints" \ + -f body="## Description\n\nAdded automatic OpenAPI spec generation for all REST endpoints.\n\n## Type of Change\n\n- [x] New feature\n\n## Testing\n\n- [x] Unit tests included\n\n## Coverage\n\n- [x] 100% line coverage maintained\n\n## Checklist\n\n- [x] Conventional Commit style applied\n" +``` + +## Guardrails + +- Always scan project rules by reading `AGENTS.md` (or `README.md`) from the project root before drafting — conventions may vary per project +- Synthesize title and description from the delta between branch and target — take all changes into account +- Never leave PR template sections blank; use `N/A` +- Use `gh api`, never `gh pr edit` +- Keep titles under 72 characters (Conventional Commits standard) +- If the PR doesn't exist or the user can't identify it, ask for clarification + +## Gotchas + +- **Never use `gh pr edit`.** It fails in this repo. Always use `gh api` to update the PR. +- **Coverage is a claim, not a verification.** The skill instructs to "confirm 100% line coverage maintained" but does not actually run tests. If code changes were made, run `npm run coverage` and check the output before making the claim. +- **Title priority is strict.** When multiple commit types exist, use the highest-priority type: fix > feat > docs > test > refactor > chore. diff --git a/.skills/update-semver/SKILL.md b/.skills/update-semver/SKILL.md new file mode 100644 index 000000000..842c58f1d --- /dev/null +++ b/.skills/update-semver/SKILL.md @@ -0,0 +1,396 @@ +--- +name: update-semver +description: Audits the delta between HEAD and the tag matching the current package.json version, decides if it's a major/minor/patch bump, updates package.json, runs npm i and npm run changelog, then triggers commit-push, enables auto-merge on the PR, and announces the new version. Does NOT create a git tag — that is handled by the separate git-tag skill after the PR merges. +license: BSD 3-Clause +compatibility: Requires Node.js 24+, npm, git CLI with remote access, gh CLI for PR management, and git worktree support. Must be run from the project root directory containing package.json. +metadata: + agent: coding +--- + +# Update Semver + +> **⚠️ EXECUTION RULE:** Every code block in this skill is a shell command to **execute**. Do not print them as text, explain them, or treat them as examples — run them directly. + +You are the release conductor. The version number is the promise you make to the world. Treat it with precision. Follow these steps in order. + +## Step 1: Ensure Clean State & Create Isolated Worktree + +Before touching any files, create a dedicated git worktree so the release work is isolated from the main working tree. This keeps `main` pristine and gives the PR a clear home. + +**Capture the project root and repository info (needed for worktree paths and gh commands):** + +```bash +PROJECT_ROOT=$(git rev-parse --show-toplevel) +GIT_REMOTE=$(git remote get-url origin 2>/dev/null) +if echo "$GIT_REMOTE" | grep -q '^git@'; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*@[^:]*:\(.*\).git$/\1/') +elif echo "$GIT_REMOTE" | grep -q 'github\.com'; then + GH_REPO=$(echo "$GIT_REMOTE" | sed 's/.*github\.com[/:]\(.*\).git$/\1/') +fi + +if [ -z "$GH_REPO" ]; then + echo "ERROR: Could not determine repository from git remote '$GIT_REMOTE'." + exit 1 +fi +echo "GH_REPO=$GH_REPO" +``` + +**Create a dedicated worktree directory as a sibling of the repo root.** Placing it *outside* the repo (e.g., `madz.worktrees/` next to `madz/`) avoids nesting a git repository inside the main working tree. A nested worktree is a known git footgun: it shows up as a nested repo to `git status`/`git clean`, and requires a `.gitignore` entry to hide it. A sibling directory keeps the repo pristine — no ignore entry needed, `git clean -fdx` is a no-op, and the worktree is still tracked normally via the `.git` file pointing back into the repo's `.git/worktrees/`. + +```bash +WORKTREES_DIR="$(dirname "$PROJECT_ROOT")/$(basename "$PROJECT_ROOT").worktrees" +mkdir -p "$WORKTREES_DIR" +``` + +```bash +# Only checkout main if not already on it +if [ "$(git branch --show-current)" != "main" ]; then + # Verify main exists on remote before pulling + REMOTE_MAIN=$(git ls-remote --heads origin main 2>/dev/null) + if [ -n "$REMOTE_MAIN" ]; then + git fetch origin main + git checkout main + git pull origin main + else + echo "WARNING: No remote branch 'main' found. Current branch '$(git branch --show-current)' will be used as-is." + fi +fi +``` + +Verify the working tree is clean: +```bash +git status --porcelain +``` + +If there are uncommitted changes, report them and stop. Do not proceed with a dirty tree. + +**Capture a unique session identifier** so that multiple instances (e.g., subagents) can run in parallel without file collisions: + +```bash +# Portable: works in Alpine, minimal images, and standard Linux +# Generates 8 random hex chars from /dev/urandom, falls back to $RANDOM if unavailable +SESSION_ID=$(head -c 8 /dev/urandom 2>/dev/null | od -An -tx1 | tr -d ' \n' || echo $RANDOM$RANDOM$RANDOM) +echo "SESSION_ID=$SESSION_ID" +``` + +**Create the isolated worktree from `main`:** + +```bash +WORKTREE_PATH="${WORKTREES_DIR}/${SESSION_ID}" + +# Create the worktree from main (--detach since main is already checked out) +git worktree add --detach "$WORKTREE_PATH" main + +# Verify worktree creation succeeded +if [ ! -d "$WORKTREE_PATH" ]; then + echo "ERROR: Worktree creation failed at $WORKTREE_PATH." + exit 1 +fi + +# Record the original directory so we can return for cleanup +ORIGINAL_DIR=$(pwd) +echo "WORKTREE_PATH=$WORKTREE_PATH" +``` + +**Change into the worktree** — all subsequent steps run from here: + +```bash +cd "$WORKTREE_PATH" +``` + +**Cleanup:** When the task completes, or when cleaning up after a failure or interruption, the agent must remove the worktree. Do not rely on a shell `trap` — explicitly clean up work files at the end of the task or during failure cleanup. Remove the worktree with `git worktree remove "$WORKTREE_PATH" --force` (falling back to `rm -rf "$WORKTREE_PATH"` if needed) and return to `$ORIGINAL_DIR`. + +## Step 1.5: Create a Release Branch + +Create a deterministic branch for this release inside the worktree. This gives the PR a clear home. + +```bash +TIMESTAMP=$(date -u +"%Y-%m-%dT%H-%M-%SZ") +BRANCH="chore/update-semver-${TIMESTAMP}" +git checkout -b "$BRANCH" +echo "Branch: $BRANCH" +``` + +The timestamp uses ISO 8601 format in UTC. Colons are replaced with dashes (`%H-%M-%S` instead of `%H:%M:%S`) to ensure filesystem compatibility. + +## Step 2: Read the Current Version + +Extract the version from `package.json` using `jq` for precision: + +```bash +CURRENT_VERSION=$(jq -r '.version' package.json) +echo "CURRENT_VERSION=$CURRENT_VERSION" +``` + +If `jq` is unavailable, fall back to `grep`: + +```bash +CURRENT_VERSION=$(grep '"version"' package.json | head -1 | sed 's/.*: *"\([^"]*\)".*/\1/') +echo "CURRENT_VERSION=$CURRENT_VERSION" +``` + +## Step 3: Find the Last Tag + +The tag to compare against is the most recent tag on the current branch — the actual last release. + +```bash +TARGET_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") +echo "TARGET_TAG=$TARGET_TAG" +``` + +If no tag exists, set `TARGET_TAG=""` and use the entire history as the delta. + +## Step 4: Audit the Delta + +Collect commit messages between the target tag and HEAD: + +```bash +if [ -n "$TARGET_TAG" ]; then + COMMITS=$(git log --pretty=format:"%s" "${TARGET_TAG}..HEAD") +else + COMMITS=$(git log --pretty=format:"%s" --max-count=50) +fi +echo "COMMITS=$COMMITS" +``` + +## Step 5: Decide the Bump + +Analyze the commit messages using **Conventional Commits** semantics. Apply the **highest** bump found, accounting for reverts: + +| Commit prefix | Bump | +|---|---| +| `feat:` | **minor** | +| `fix:` | **patch** | +| `BREAKING CHANGE` or `!` after type/scope | **major** | +| `perf:` | **minor** (performance improvements are treated as features) | +| `refactor:`, `chore:`, `docs:`, `style:`, `test:` | **no bump** | +| `Revert "feat: ..."` | **cancels** the corresponding `feat:` | +| `Revert "fix: ..."` | **cancels** the corresponding `fix:` | + +**Decision rules:** +1. Count `feat:` commits and `Revert "feat: ..."` commits. Net features = feat count minus revert count. +2. Count `fix:` commits and `Revert "fix: ..."` commits. Net fixes = fix count minus revert count. +3. If net features > 0 → **minor** +4. If net fixes > 0 (and no net features) → **patch** +5. If no version-relevant commits but commits exist → **patch** (maintenance release) +6. If no commits at all → **abort** (nothing to release) + +Implement the counting and bump decision: + +```bash +# Count commit types (excluding merge commits and reverts) +# Patterns match both "type:" and "type(scope):" conventional commit formats +FEAT_COUNT=$(echo "$COMMITS" | grep -cE "^feat(\(|:)" || true) +FIX_COUNT=$(echo "$COMMITS" | grep -cE "^fix(\(|:)" || true) +PERF_COUNT=$(echo "$COMMITS" | grep -cE "^perf(\(|:)" || true) +BREAKING_COUNT=$(echo "$COMMITS" | grep -cE "(BREAKING CHANGE|!:)" || true) +REVERT_FEAT_COUNT=$(echo "$COMMITS" | grep -cE '^Revert "feat' || true) +REVERT_FIX_COUNT=$(echo "$COMMITS" | grep -cE '^Revert "fix' || true) + +# Net counts after reverts +NET_FEAT=$((FEAT_COUNT - REVERT_FEAT_COUNT)) +NET_FIX=$((FIX_COUNT - REVERT_FIX_COUNT)) + +# Determine bump +if [ "$BREAKING_COUNT" -gt 0 ]; then + BUMP="major" + REASON="$BREAKING_COUNT breaking change(s)" +elif [ "$NET_FEAT" -gt 0 ] || [ "$PERF_COUNT" -gt 0 ]; then + BUMP="minor" + REASON="$NET_FEAT unreverted feat(s)" +elif [ "$NET_FIX" -gt 0 ]; then + BUMP="patch" + REASON="$NET_FIX unreverted fix(es)" +elif [ -n "$COMMITS" ]; then + BUMP="patch" + REASON="no version-relevant commits ($(echo "$COMMITS" | wc -l) commits)" +else + echo "No commits to release. Aborting." + exit 1 +fi + +**Structured decision:** If the harness provides a structured-decision tool (e.g., `decision`), use it to pick the bump instead of reasoning it out manually. Feed the commit delta as state. + +``` +Question: "What semver bump should this release get?" +Type: choice +Options: major, minor, patch, none +Criteria: + major = BREAKING CHANGE or `!` after type/scope + minor = net feat > 0 or perf commits + patch = net fix > 0 or no version-relevant commits + none = no commits at all +Example answer: minor +``` + +Interpret the returned label directly. Fall back to the counting rules above if no decision tool is available. + +# Compute new version using POSIX-compatible field splitting +MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1) +MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2) +PATCH=$(echo "$CURRENT_VERSION" | cut -d. -f3) +case "$BUMP" in + major) NEW_VERSION="$((MAJOR + 1)).0.0" ;; + minor) NEW_VERSION="${MAJOR}.$((MINOR + 1)).0" ;; + patch) NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" ;; +esac + +echo "Bump: $BUMP ($REASON)" +echo "Version: $CURRENT_VERSION → $NEW_VERSION" +echo "NEW_VERSION=$NEW_VERSION" +echo "BUMP=$BUMP" +``` + +## Step 6: Update package.json + +Replace the version in `package.json` with the new semver value. Use `jq` for precision: + +```bash +jq --arg v "<NEW_VERSION>" '.version = $v' package.json > package.json.tmp && mv package.json.tmp package.json +``` + +If `jq` is unavailable, fall back to `sed` — but verify there's only one version field: + +```bash +VERSION_COUNT=$(grep -c '"version"' package.json) +if [ "$VERSION_COUNT" -ne 1 ]; then + echo "ERROR: Found $VERSION_COUNT version fields in package.json. Cannot proceed safely." + exit 1 +fi +sed 's/"version": "[^"]*"/"version": "<NEW_VERSION>"/' package.json > package.json.tmp && mv package.json.tmp package.json +``` + +Verify the change: +```bash +jq -r '.version' package.json +``` + +## Step 7: Install, Build, and Generate Changelog + +```bash +npm i --ignore-scripts +if [ $? -ne 0 ]; then + echo "ERROR: npm install failed. Aborting." + exit 1 +fi + +# Run build if the script exists — MUST succeed if present +if jq -e '.scripts.build' package.json > /dev/null 2>&1; then + echo "Build script detected. Running build..." + npm run build + if [ $? -ne 0 ]; then + echo "ERROR: Build script failed. Aborting — cannot release a broken build." + exit 1 + fi + echo "Build succeeded." +fi + +npm run changelog +if [ $? -ne 0 ]; then + echo "WARNING: npm run changelog failed. Continuing without changelog update." +fi +``` + +This installs dependencies (ensuring lockfile is current), runs `build` if the project has one (**must succeed** if present — a broken build is a broken release), and generates an updated `CHANGELOG.md` using `auto-changelog`. The `--ignore-scripts` flag prevents postinstall scripts from running during the version bump. If `npm i` fails, abort immediately — do not proceed to commit a broken state. + +## Step 8: Trigger commit-push + +Delegating version release to `commit-push`, which will: +1. Scan for AGENTS.md to read project rules +2. Stage **all** files (`git add -A` — nothing left behind) +3. Commit with a conventional commit message (e.g., `chore: release v1.3.8`) +4. Push to the remote +5. Open a PR targeting `main` + +Invoke the `commit-push` skill. + +**After `commit-push` completes, continue to Step 9.** Do not stop or wait for further input — the pipeline proceeds automatically. + +Add a reusable capture pattern to extract `PR_NUMBER` from the conversation history after `commit-push` completes: + +```bash +# Capture PR_NUMBER from the conversation history (last occurrence wins) +PR_NUMBER=$(echo "$CONVERSATION_HISTORY" | sed -n 's/^PR_NUMBER=\([0-9]\{1,\}\)$/\1/p' | tail -1) +if [ -z "$PR_NUMBER" ]; then + echo "ERROR: Could not capture PR_NUMBER from commit-push output." + exit 1 +fi +echo "PR_NUMBER=$PR_NUMBER" +``` + +## Step 9: Enable Auto-Merge + +After the PR is created, enable auto-merge on it with **squash** merge. Extract the PR number from the `commit-push` output (look for `PR_NUMBER=<number>` printed as structured output), then enable auto-merge: + +```bash +gh pr merge "$PR_NUMBER" --auto --squash +echo "Auto-merge: ENABLED (squash)" + +# Verify auto-merge was enabled successfully +AUTO_MERGE=$(gh pr view "$PR_NUMBER" --json autoMerge --jq '.enabled' 2>/dev/null || true) +if [ "$AUTO_MERGE" != "true" ]; then + echo "WARNING: Auto-merge may not be enabled for PR #$PR_NUMBER (state: $AUTO_MERGE)." +else + echo "Auto-merge verified for PR #$PR_NUMBER." +fi +``` + +If auto-merge fails (e.g., repo settings don't support it), report the error and advise manual merge. + +## Step 10: Announce the New Version + +Print a final announcement: + +``` +Version <NEW_VERSION> is live. + +Branch: <branch-name> +PR: <PR_URL> +Bump: <type> (<count> commits analyzed) +From: <TARGET_TAG or "initial"> +Changelog: updated + +The world gets a new version. +``` + +## Examples + +``` +User: update-semver + +Agent: Creating release branch: chore/update-semver-2026-08-15T10-30-00Z +Current version: 1.34.0 +Last tag: 1.34.0 +Delta: 12 commits + +Bump: minor (3 feat: commits found) +Version: 1.34.0 → 1.35.0 + +Updated package.json to 1.35.0 +npm install: success +Build: success +Changelog: updated + +commit-push: PR #456 created +Auto-merge: ENABLED (squash) + +Version 1.35.0 is live. + +Branch: chore/update-semver-2026-08-15T10-30-00Z +PR: https://github.com/avoidwork/madz/pull/456 +Bump: minor (3 commits analyzed) +From: 1.34.0 +Changelog: updated + +The world gets a new version. +``` + +## Gotchas + +- **Worktree isolation.** All release work happens in an isolated worktree created in Step 1. The worktree is a sibling of the repo root (e.g., `madz.worktrees/<SESSION_ID>/`), not nested inside it — this avoids the nested-repo footgun and keeps `main` pristine. +- **Cleanup is the agent's responsibility.** When the task completes, or when cleaning up after a failure or interruption, remove the worktree as described in Step 1. Do not rely on a shell `trap` — explicitly clean up work files at the end of the task or during failure cleanup. +- **Session IDs prevent collisions.** When running multiple instances (e.g., subagents), always use a unique `SESSION_ID` to avoid worktree path collisions. +- **`commit-push` is delegated, not inline.** Step 8 invokes `commit-push` as a chain instruction — do not perform git operations inline at that step. +- **Never commit directly to `main`.** The release branch is created inside the worktree in Step 1.5; `main` is never modified directly. + +--- diff --git a/coverage.txt b/coverage.txt index 688beecf0..9f86bd69c 100644 --- a/coverage.txt +++ b/coverage.txt @@ -74,7 +74,7 @@ ℹ skills | | | | ℹ agentMapper.js | 83.33 | 62.50 | 100.00 | 12-13 23-25 ℹ discoverer.js | 99.10 | 92.31 | 100.00 | 174-175 -ℹ registry.js | 91.37 | 80.00 | 83.33 | 104-105 155-156 173-175 201-207 215-219 227-231 +ℹ registry.js | 91.37 | 71.88 | 83.33 | 104-105 155-156 173-175 201-207 215-219 227-231 ℹ types.js | 100.00 | 100.00 | 100.00 | ℹ validator.js | 100.00 | 100.00 | 80.00 | ℹ stream | | | | @@ -231,6 +231,6 @@ ℹ loadAgents.js | 88.00 | 42.86 | 100.00 | 17-18 32-33 38-39 ℹ project.js | 100.00 | 100.00 | 100.00 | ℹ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -ℹ all files | 91.53 | 87.57 | 86.84 | +ℹ all files | 91.53 | 87.54 | 86.84 | ℹ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ℹ end of coverage report From 6bb04ce44b3b5f0769f17f93aeac6477aff921fb Mon Sep 17 00:00:00 2001 From: Jason Mulligan <jason.mulligan@avoidwork.com> Date: Sat, 31 Oct 2026 08:57:11 -0400 Subject: [PATCH 2/6] docs: document bundled skills --- docs/README.md | 2 ++ docs/SKILLS.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 docs/SKILLS.md diff --git a/docs/README.md b/docs/README.md index 4cc726ef4..f444b5ac4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ The documentation for the madz AI harness. This directory is organized for reade | **Look up a feature, tool, or agent** | [FEATURES.md](./FEATURES.md) | | **Look up a config key** | [CONFIGURATION.md](./CONFIGURATION.md) | | **Set up for development / write skills** | [DEVELOPMENT.md](./DEVELOPMENT.md) | +| **Look up a bundled skill** | [SKILLS.md](./SKILLS.md) | | **Understand the architecture** (how subsystems fit together) | [OVERVIEW.md](./OVERVIEW.md) | | **Trace how the code actually runs** (call chains, data flows) | [FLOWS.md](./FLOWS.md) | | **Understand the terminal interface** | [TUI.md](./TUI.md) and [TUI_FLOWS.md](./TUI_FLOWS.md) | @@ -29,6 +30,7 @@ The documentation for the madz AI harness. This directory is organized for reade | [FEATURES.md](./FEATURES.md) | All | Nothing | Providers, agents, tools, MCP, skills, memory, sandbox, vector search, cron | | [CONFIGURATION.md](./CONFIGURATION.md) | Operator | Nothing | `config.yaml` reference, logging, status bar, summarization | | [DEVELOPMENT.md](./DEVELOPMENT.md) | Contributor | Node.js | Testing, setup, extending skills, directory layout | +| [SKILLS.md](./SKILLS.md) | All | Nothing | The bundled skills shipped in `.skills/` | | [OVERVIEW.md](./OVERVIEW.md) | Intermediate → Advanced | TUTORIAL | Architecture, subsystems, data flows | | [FLOWS.md](./FLOWS.md) | Advanced | OVERVIEW | Call chains and data flows for every primary code path | | [TUI.md](./TUI.md) | Intermediate → Advanced | React, Ink | TUI design blueprint, current implementation, proposed features | diff --git a/docs/SKILLS.md b/docs/SKILLS.md new file mode 100644 index 000000000..f39b72116 --- /dev/null +++ b/docs/SKILLS.md @@ -0,0 +1,61 @@ +# Bundled Skills + +The skills that ship with madz in `.skills/`. Each is an [Agent Skills](https://agentskills.io/specification)-spec-compliant `SKILL.md` that the orchestrator loads on demand via the `skill` tool. They are scanned first and shadow user skills of the same name in `skills/`. + +## How Skills Work + +Skills are instruction files interpreted by the agent — they are not spawned into a separate process. When a task matches a skill's `description`, the orchestrator loads the full `SKILL.md` body and follows its workflow. A skill can carry an optional `scripts/` directory with executable helpers (`.py`, `.sh`, `.js`, `.rb`, `.ts`). + +Skills are grouped by the agent that owns them (declared in frontmatter `metadata.agent`). The catalog below groups them by workflow domain. + +## Git & Release Workflow + +| Skill | Description | +|-------|-------------| +| `commit-push` | Automates the complete git workflow: scans project rules for conventions, stages all changes, commits, pushes to the remote, and opens a Pull Request. Ensures strict compliance with documentation rules, including rule 5.4. | +| `git-tag` | Reads version from `package.json` (Node.js), `pyproject.toml` (Python), `Cargo.toml` (Rust), `build.gradle` (Java/Android), `pom.xml` (Maven), `build.gradle.kts` (Gradle Kotlin), `pubspec.yaml` (Dart), `Gemfile` (Ruby), or similar package manager files. Synthesizes a change description from git history delta, creates an annotated git tag (no `v` prefix), and pushes it to origin. | +| `update-semver` | Audits the delta between HEAD and the tag matching the current `package.json` version, decides if it's a major/minor/patch bump, updates `package.json`, runs `npm i` and `npm run changelog`, then triggers commit-push, enables auto-merge on the PR, and announces the new version. Does NOT create a git tag — that is handled by the separate `git-tag` skill after the PR merges. | +| `release-madz` | Builds and pushes all Docker images for the madz project by running `npm run docker:release:all`. Runs the build as a background process with a 30-minute timeout. Reports completion status to the user. NOT retryable — Docker image tags are immutable, so a failed run cannot be re-run against the same version. | +| `purge-branches` | Deletes all local git branches except `main`. Works in any git repository — no npm or `package.json` dependency. | +| `update-pr` | Update an existing PR's title and description by scanning project rules for conventions and the PR template, synthesizing both from the delta between the branch and target, then applying changes via `gh api` (`gh pr edit` fails in this repo). | + +## Issue & Feature Lifecycle + +| Skill | Description | +|-------|-------------| +| `create-issue` | Receives a user description, synthesizes it into a title and description, categorizes as `fix` or `feat`, creates a GitHub issue, audits the codebase for actionable details, and updates the issue with findings. | +| `fix-issue` | Accepts a GitHub issue ID, validates approval via labels, categorizes the work, chains to `/create-feature` for full implementation, and comments on the issue linking to the PR. | +| `create-feature` | Orchestrates a complete feature lifecycle: receive goals, synthesize specs, propose via OpenSpec, commit specs to PR, apply tasks, commit implementation, archive, update PR, and post audit results. Creates an isolated git worktree for each run. | +| `scan-issues` | Scans GitHub for open issues labeled `approved` (excluding those also labeled `in progress`), processes them via `fix-issue`, opens PRs with auto-merge, and resolves merge conflicts. Runs hourly. | + +## OpenSpec Workflow + +| Skill | Description | +|-------|-------------| +| `openspec-propose` | Propose a new change with all artifacts generated in one step. Use when the user wants to quickly describe what they want to build and get a complete proposal with design, specs, and tasks ready for implementation. | +| `openspec-apply-change` | Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks. | +| `openspec-archive-change` | Archive a completed change in the experimental workflow. Use when the user wants to finalize and archive a change after implementation is complete. | +| `openspec-explore` | Enter explore mode — a thinking partner for exploring ideas, investigating problems, and clarifying requirements. Use when the user wants to think through something before or during a change. | + +## Audit & Review + +| Skill | Description | +|-------|-------------| +| `audit-code` | Audits each directory in `./src` for bugs, security vulnerabilities, and performance issues. Generates one consolidated issue per directory via `/create-issue`. | +| `audit-skill` | Audits Agent Skills `SKILL.md` files against the protocol specification and best practices. Identifies issues across 10 categories: schema violations, naming, description quality, structure, progressive disclosure, file references, best practices, calibration, content quality, and optional directories. Produces a structured report with severity levels and actionable fixes. | +| `audit-sys-prompt` | Expert System Prompt Evaluator Agent. Analyzes, rates, and provides actionable feedback for system prompts against a 7-criteria framework. Returns structured machine-readable JSON output with weighted scores, evidence-based reasoning, and harness-aware recommendations. | +| `restructure-code` | Audits each directory in `./src` for opportunities to restructure directories to align with community best practices & better organization in general. Generates one consolidated issue per directory via `/create-issue`. | + +## Reflection + +| Skill | Description | +|-------|-------------| +| `reflection` | Generate a narrative reflection summary from recent session history to capture mood, energy, and interaction quality. | + +## Invoking a Skill + +Skills are invoked by the orchestrator when a task matches the skill's `description`. They can also be triggered directly from a message (e.g., `/commit-push`) or scheduled via the cron system (`node index.js --message "Run the reflection skill"`). + +## Adding a Skill + +See [DEVELOPMENT.md](./DEVELOPMENT.md) for how to create skills programmatically via the `createSkill` tool or manually under `skills/`. Bundled skills live in `.skills/` and are committed to the repo. From 53ee49700dc7efde542c8e59dab1011a4990f565 Mon Sep 17 00:00:00 2001 From: Jason Mulligan <jason.mulligan@avoidwork.com> Date: Sat, 31 Oct 2026 09:09:08 -0400 Subject: [PATCH 3/6] docs: remove release-madz from bundled skills catalog --- docs/SKILLS.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/SKILLS.md b/docs/SKILLS.md index f39b72116..cff8c93d4 100644 --- a/docs/SKILLS.md +++ b/docs/SKILLS.md @@ -15,7 +15,6 @@ Skills are grouped by the agent that owns them (declared in frontmatter `metadat | `commit-push` | Automates the complete git workflow: scans project rules for conventions, stages all changes, commits, pushes to the remote, and opens a Pull Request. Ensures strict compliance with documentation rules, including rule 5.4. | | `git-tag` | Reads version from `package.json` (Node.js), `pyproject.toml` (Python), `Cargo.toml` (Rust), `build.gradle` (Java/Android), `pom.xml` (Maven), `build.gradle.kts` (Gradle Kotlin), `pubspec.yaml` (Dart), `Gemfile` (Ruby), or similar package manager files. Synthesizes a change description from git history delta, creates an annotated git tag (no `v` prefix), and pushes it to origin. | | `update-semver` | Audits the delta between HEAD and the tag matching the current `package.json` version, decides if it's a major/minor/patch bump, updates `package.json`, runs `npm i` and `npm run changelog`, then triggers commit-push, enables auto-merge on the PR, and announces the new version. Does NOT create a git tag — that is handled by the separate `git-tag` skill after the PR merges. | -| `release-madz` | Builds and pushes all Docker images for the madz project by running `npm run docker:release:all`. Runs the build as a background process with a 30-minute timeout. Reports completion status to the user. NOT retryable — Docker image tags are immutable, so a failed run cannot be re-run against the same version. | | `purge-branches` | Deletes all local git branches except `main`. Works in any git repository — no npm or `package.json` dependency. | | `update-pr` | Update an existing PR's title and description by scanning project rules for conventions and the PR template, synthesizing both from the delta between the branch and target, then applying changes via `gh api` (`gh pr edit` fails in this repo). | From ac9c956f24f7c3bafd532d9300b0ca6c05e447bd Mon Sep 17 00:00:00 2001 From: Jason Mulligan <jason.mulligan@avoidwork.com> Date: Sat, 31 Oct 2026 09:23:02 -0400 Subject: [PATCH 4/6] feat: add agent.bundledSkills config to gate bundled skills --- config.yaml | 1 + coverage.txt | 6 +-- docs/CONFIGURATION.md | 2 + docs/SKILLS.md | 2 + src/config/schemas/agent.js | 1 + src/skills/discoverer.js | 13 +++++- src/skills/registry.js | 5 ++- tests/unit/config/schemas/agent.test.js | 20 +++++++++ tests/unit/skills/discoverer.test.js | 55 +++++++++++++++++++++++++ 9 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 tests/unit/config/schemas/agent.test.js diff --git a/config.yaml b/config.yaml index 48251d546..0f5de8616 100644 --- a/config.yaml +++ b/config.yaml @@ -166,6 +166,7 @@ agent: baseUrl: "" # empty = tool not registered model: tev1:4b sampling: true # false = tool not registered + bundledSkills: true # false = only 'reflection' loaded from .skills/ summarization: enabled: true trigger: diff --git a/coverage.txt b/coverage.txt index 9f86bd69c..7eb9a9dfa 100644 --- a/coverage.txt +++ b/coverage.txt @@ -73,8 +73,8 @@ ℹ logger.js | 79.53 | 66.67 | 100.00 | 87-98 123-132 137 139-141 164-166 176-180 203-211 217-222 239 273-274 276-278 297-299 307-309 317-319 327-329 337-339 ℹ skills | | | | ℹ agentMapper.js | 83.33 | 62.50 | 100.00 | 12-13 23-25 -ℹ discoverer.js | 99.10 | 92.31 | 100.00 | 174-175 -ℹ registry.js | 91.37 | 71.88 | 83.33 | 104-105 155-156 173-175 201-207 215-219 227-231 +ℹ discoverer.js | 99.13 | 92.75 | 100.00 | 174-175 +ℹ registry.js | 91.46 | 69.70 | 83.33 | 107-108 158-159 176-178 204-210 218-222 230-234 ℹ types.js | 100.00 | 100.00 | 100.00 | ℹ validator.js | 100.00 | 100.00 | 80.00 | ℹ stream | | | | @@ -231,6 +231,6 @@ ℹ loadAgents.js | 88.00 | 42.86 | 100.00 | 17-18 32-33 38-39 ℹ project.js | 100.00 | 100.00 | 100.00 | ℹ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -ℹ all files | 91.53 | 87.54 | 86.84 | +ℹ all files | 91.53 | 87.53 | 86.85 | ℹ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ℹ end of coverage report diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 5ecfbf4ee..85f135a0c 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -83,6 +83,7 @@ Graceful shutdown flushes all buffered log entries to disk before process exit. | | `statusBar.version` | `true` | Show the version in the status bar | | `agent` | `recursionLimit` | `1000` | Max graph execution steps per agent call | | | `sampling` | `true` | Enable the `sampling` tool (ephemeral memory capture); `false` disables registration | +| | `bundledSkills` | `true` | Load bundled `.skills/` skills; `false` loads only `reflection` from `.skills/` | | `mcp` | `<name>.transport` | _(none)_ | MCP transport (`stdio`, `http`, `sse`) | | | `<name>.command` | _(none)_ | Command for `stdio` servers | | | `<name>.args` | `[]` | Args for `stdio` servers | @@ -170,4 +171,5 @@ On a `429` rate-limit response, the request is retried once after the `retry-aft | `SKILL_AGENT_MAP_1_PATTERN` | _(none)_ | Second skill-to-agent pattern | | `SKILL_AGENT_MAP_1_AGENT` | _(none)_ | Second skill-to-agent mapping | | `AGENT_SAMPLING` | `true` | Enable the `sampling` tool; `false` disables registration | +| `AGENT_BUNDLED_SKILLS` | `true` | Load bundled `.skills/` skills; `false` loads only `reflection` | diff --git a/docs/SKILLS.md b/docs/SKILLS.md index cff8c93d4..1d70e6014 100644 --- a/docs/SKILLS.md +++ b/docs/SKILLS.md @@ -8,6 +8,8 @@ Skills are instruction files interpreted by the agent — they are not spawned i Skills are grouped by the agent that owns them (declared in frontmatter `metadata.agent`). The catalog below groups them by workflow domain. +**Bundled skills toggle:** Set `agent.bundledSkills: false` in `config.yaml` (or `AGENT_BUNDLED_SKILLS=false`) to load only the `reflection` skill from `.skills/`. The other bundled skills are dev/release tooling that isn't required for day-to-day operation; user skills in `skills/` are unaffected. + ## Git & Release Workflow | Skill | Description | diff --git a/src/config/schemas/agent.js b/src/config/schemas/agent.js index 322e4b14c..fa2d2faf7 100644 --- a/src/config/schemas/agent.js +++ b/src/config/schemas/agent.js @@ -9,4 +9,5 @@ export const AgentSchema = z.object({ recursionLimit: z.number().int().positive().default(1000), decision: DecisionSchema.default({}), sampling: z.boolean().default(true), + bundledSkills: z.boolean().default(true), }); diff --git a/src/skills/discoverer.js b/src/skills/discoverer.js index 4fabf8f15..ddadc214b 100644 --- a/src/skills/discoverer.js +++ b/src/skills/discoverer.js @@ -182,11 +182,13 @@ async function findSkillFiles(dir) { * @param {string[]} [scope] - Array of directories to scan (defaults to sandbox.skillScanPaths from config) * @param {object} [options] - Discovery options * @param {boolean} [options.trustProjectSkills=true] - Whether to trust project-level skills + * @param {boolean} [options.bundledSkills=true] - Whether to load bundled `.skills/` skills. When `false`, + * only the `reflection` skill is loaded from the `.skills/` scope; user skills in `skills/` are unaffected. * @returns {Promise<Array<{ path: string, name: string, metadata: Object }>>} */ export async function discoverSkills(scope = defaultScope, options = {}) { const cwdParam = options.cwd || cwd; - const { trustProjectSkills: _trustProjectSkills = true } = options; + const { trustProjectSkills: _trustProjectSkills = true, bundledSkills = true } = options; const allSkills = []; const seenNames = new Map(); @@ -198,7 +200,14 @@ export async function discoverSkills(scope = defaultScope, options = {}) { continue; } - const skills = await findSkillFiles(fullScope); + let skills = await findSkillFiles(fullScope); + + // When bundled skills are disabled, only the `reflection` skill is + // loaded from the system `.skills/` scope. User skills in `skills/` + // are unaffected. + if (!bundledSkills && scopePath.includes(".skills")) { + skills = skills.filter((skill) => skill.name === "reflection"); + } for (const skill of skills) { const name = skill.metadata.name || skill.name; diff --git a/src/skills/registry.js b/src/skills/registry.js index e8faeff64..1f8f6ac9d 100644 --- a/src/skills/registry.js +++ b/src/skills/registry.js @@ -32,10 +32,13 @@ export class SkillRegistry { * @param {string[]} [scope] - Array of directories to scan (defaults to sandbox.skillScanPaths from config) * @param {object} [options] - Discovery options * @param {boolean} [options.trustProjectSkills=true] - Trust project-level skills + * @param {boolean} [options.bundledSkills=true] - When `false`, only the `reflection` skill is loaded from `.skills/` * @returns {Promise<Array<{ name: string, errors: string[], warnings: string[] }>>} Registration results */ async discover(scope = defaultScope, options = {}) { - const discovered = await discoverSkills(scope, options); + // Default bundledSkills from config when not explicitly passed. + const bundledSkills = options.bundledSkills ?? config.agent?.bundledSkills ?? true; + const discovered = await discoverSkills(scope, { ...options, bundledSkills }); const results = []; for (const skill of discovered) { diff --git a/tests/unit/config/schemas/agent.test.js b/tests/unit/config/schemas/agent.test.js new file mode 100644 index 000000000..ae0bff31d --- /dev/null +++ b/tests/unit/config/schemas/agent.test.js @@ -0,0 +1,20 @@ +import { describe, it } from "node:test"; +import assert from "node:assert"; +import { AgentSchema } from "../../../../src/config/schemas/agent.js"; + +describe("AgentSchema", () => { + it("defaults bundledSkills to true", () => { + const result = AgentSchema.parse({}); + assert.strictEqual(result.bundledSkills, true); + }); + + it("accepts bundledSkills false", () => { + const result = AgentSchema.parse({ bundledSkills: false }); + assert.strictEqual(result.bundledSkills, false); + }); + + it("defaults sampling to true", () => { + const result = AgentSchema.parse({}); + assert.strictEqual(result.sampling, true); + }); +}); diff --git a/tests/unit/skills/discoverer.test.js b/tests/unit/skills/discoverer.test.js index 73f7f3abe..8869d6e0f 100644 --- a/tests/unit/skills/discoverer.test.js +++ b/tests/unit/skills/discoverer.test.js @@ -152,3 +152,58 @@ describe("discoverSkills with --- in body", () => { assert.strictEqual(skills[0].metadata.description, "A skill with a table"); }); }); + +// --- discoverSkills bundledSkills filtering --- + +describe("discoverSkills bundledSkills filtering", () => { + beforeEach(setup); + afterEach(cleanup); + + it("loads all skills from .skills/ by default", async () => { + const systemDir = join(testDir, ".skills"); + mkdirSync(join(systemDir, "reflection"), { recursive: true }); + mkdirSync(join(systemDir, "commit-push"), { recursive: true }); + writeFileSync( + join(systemDir, "reflection", "SKILL.md"), + "---\nname: reflection\ndescription: A reflection skill\n---\n\nBody", + ); + writeFileSync( + join(systemDir, "commit-push", "SKILL.md"), + "---\nname: commit-push\ndescription: A commit skill\n---\n\nBody", + ); + + const skills = await discoverSkills([".skills"]); + assert.strictEqual(skills.length, 2); + }); + + it("loads only reflection from .skills/ when bundledSkills is false", async () => { + const systemDir = join(testDir, ".skills"); + mkdirSync(join(systemDir, "reflection"), { recursive: true }); + mkdirSync(join(systemDir, "commit-push"), { recursive: true }); + writeFileSync( + join(systemDir, "reflection", "SKILL.md"), + "---\nname: reflection\ndescription: A reflection skill\n---\n\nBody", + ); + writeFileSync( + join(systemDir, "commit-push", "SKILL.md"), + "---\nname: commit-push\ndescription: A commit skill\n---\n\nBody", + ); + + const skills = await discoverSkills([".skills"], { bundledSkills: false }); + assert.strictEqual(skills.length, 1); + assert.strictEqual(skills[0].name, "reflection"); + }); + + it("does not filter user skills in skills/ when bundledSkills is false", async () => { + const userDir = join(testDir, "skills"); + mkdirSync(join(userDir, "my-skill"), { recursive: true }); + writeFileSync( + join(userDir, "my-skill", "SKILL.md"), + "---\nname: my-skill\ndescription: A user skill\n---\n\nBody", + ); + + const skills = await discoverSkills(["skills"], { bundledSkills: false }); + assert.strictEqual(skills.length, 1); + assert.strictEqual(skills[0].name, "my-skill"); + }); +}); From 8d21c5d6780fd9f4e135f71c5515b2f5ea1b9be6 Mon Sep 17 00:00:00 2001 From: Jason Mulligan <jason.mulligan@avoidwork.com> Date: Sat, 31 Oct 2026 09:28:05 -0400 Subject: [PATCH 5/6] docs: document AGENT_BUNDLED_SKILLS env var in DOCKER.md --- docs/DOCKER.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/DOCKER.md b/docs/DOCKER.md index df63c00c6..d2ad21865 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -243,6 +243,7 @@ The active provider is selected by the `enabled` flag, not by config position or | Variable | Default | Description | | ------------------------------- | -------- | ---------------------------------------------- | | `AGENT_RECURSION_LIMIT` | `1000` | Max graph execution steps per call | +| `AGENT_BUNDLED_SKILLS` | `true` | Load bundled `.skills/` skills; `false` loads only `reflection` | ### Persistence From a683c07b801dc7697587ace20d906d521659d74d Mon Sep 17 00:00:00 2001 From: Jason Mulligan <jason.mulligan@avoidwork.com> Date: Sat, 31 Oct 2026 09:31:09 -0400 Subject: [PATCH 6/6] docs: list all agent env vars in DOCKER.md sorted alphabetically --- docs/DOCKER.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/DOCKER.md b/docs/DOCKER.md index d2ad21865..3e88b6bf9 100644 --- a/docs/DOCKER.md +++ b/docs/DOCKER.md @@ -240,10 +240,13 @@ The active provider is selected by the `enabled` flag, not by config position or ### Agent -| Variable | Default | Description | -| ------------------------------- | -------- | ---------------------------------------------- | -| `AGENT_RECURSION_LIMIT` | `1000` | Max graph execution steps per call | -| `AGENT_BUNDLED_SKILLS` | `true` | Load bundled `.skills/` skills; `false` loads only `reflection` | +| Variable | Default | Description | +| ------------------------------- | ---------- | ---------------------------------------------- | +| `AGENT_BUNDLED_SKILLS` | `true` | Load bundled `.skills/` skills; `false` loads only `reflection` | +| `AGENT_DECISION_BASE_URL` | _(empty)_ | Decision model base URL; empty disables the `decision` tool | +| `AGENT_DECISION_MODEL` | `tev1:4b` | Decision model identifier | +| `AGENT_RECURSION_LIMIT` | `1000` | Max graph execution steps per call | +| `AGENT_SAMPLING` | `true` | Enable the `sampling` tool; `false` disables registration | ### Persistence