Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .architecture/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,20 @@ review_process:
# Architecture Decision Record (ADR) configuration
adr:
# Numbering format: sequential | date-based
# Sequential produces ADR-001, ADR-002, etc.
# Date-based produces ADR-20260210, ADR-20260211, etc.
numbering_format: sequential

# Sequential format: zero-padding pattern
# Count of zeros determines digit width: "000" = 3 digits, "0000" = 4 digits
# Only used when numbering_format: sequential
# sequential_format: "000"

# Date format: strftime format string passed to date command
# Only used when numbering_format: date-based
# Examples: "%Y%m%d" = 20260210, "%Y-%m-%d" = 2026-02-10
# date_format: "%Y%m%d"

# Require alternatives section
require_alternatives: true

Expand Down
12 changes: 12 additions & 0 deletions .architecture/templates/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,20 @@ review_process:
# Architecture Decision Record (ADR) configuration
adr:
# Numbering format: sequential | date-based
# Sequential produces ADR-001, ADR-002, etc.
# Date-based produces ADR-20260210, ADR-20260211, etc.
numbering_format: sequential

# Sequential format: zero-padding pattern
# Count of zeros determines digit width: "000" = 3 digits, "0000" = 4 digits
# Only used when numbering_format: sequential
# sequential_format: "000"

# Date format: strftime format string passed to date command
# Only used when numbering_format: date-based
# Examples: "%Y%m%d" = 20260210, "%Y-%m-%d" = 2026-02-10
# date_format: "%Y%m%d"

# Require alternatives section
require_alternatives: true

Expand Down
80 changes: 74 additions & 6 deletions .claude/skills/_patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ This document contains reusable patterns referenced by AI Software Architect ski

1. [Tool Permission Pattern](#tool-permission-pattern)
2. [Progressive Disclosure Pattern](#progressive-disclosure-pattern)
3. [Input Validation & Sanitization](#input-validation--sanitization)
4. [Error Handling](#error-handling)
5. [File Loading](#file-loading)
6. [Reporting Format](#reporting-format)
7. [Skill Workflow Template](#skill-workflow-template)
3. [Script-Based Deterministic Operations](#script-based-deterministic-operations)
4. [Input Validation & Sanitization](#input-validation--sanitization)
5. [Error Handling](#error-handling)
6. [File Loading](#file-loading)
7. [Reporting Format](#reporting-format)
8. [Skill Workflow Template](#skill-workflow-template)

---

Expand Down Expand Up @@ -85,7 +86,7 @@ allowed-tools: Read,Write,Edit,Glob,Grep,Bash
|-------|-------|-----------|
| `list-members` | `Read` | Only reads members.yml |
| `architecture-status` | `Read,Glob,Grep` | Scans files and searches |
| `create-adr` | `Read,Write,Bash(ls:*,grep:*)` | Creates ADRs, scans for numbering |
| `create-adr` | `Read,Write,Bash(bash:*,ls:*,grep:*)` | Creates ADRs, runs numbering script |
| `specialist-review` | `Read,Write,Glob,Grep` | Reviews code, writes reports |
| `architecture-review` | `Read,Write,Glob,Grep,Bash(git:*)` | Full reviews + git status |
| `pragmatic-guard` | `Read,Edit` | Reads/modifies config |
Expand Down Expand Up @@ -425,6 +426,67 @@ mkdir -p .claude/skills/skill-name/assets

---

## Script-Based Deterministic Operations

**Status**: Implemented (2026-02-10)
**Reference**: [ADR-009](../../.architecture/decisions/adrs/ADR-009-script-based-deterministic-operations.md)

Operations that must produce identical results regardless of LLM interpretation should be implemented as shell scripts, not as inline skill instructions. Skills invoke scripts via `Bash`, scripts return structured output.

### When to Script

**Script when:**
- Operation involves destructive actions (`rm -rf`, file overwrites)
- Operation has an exact correct output (numbering, formatting, paths)
- Operation reads configuration and must respect it deterministically
- LLM improvisation has caused or could cause bugs
- Trust erosion from incorrect results would undermine framework credibility

**Keep interpretive when:**
- Operation requires understanding context (project analysis, recommendations)
- Output is naturally variable (reviews, summaries, team customization)
- No single correct answer exists

### Script Interface Convention

Scripts follow a consistent interface pattern:

- **Arguments**: Positional, documented in script header comments
- **Stdout**: Structured tokens or single-value output (machine-readable)
- **Stderr**: Human-readable errors and warnings
- **Exit codes**: 0 = success, non-zero = specific failure category
- **Safety**: `set -euo pipefail` in all scripts

### Current Scripts

| Script | Location | Used By | Purpose |
|--------|----------|---------|---------|
| `install-framework.sh` | `setup-architect/scripts/` | `setup-architect` | All installation file operations |
| `next-adr-number.sh` | `scripts/` (shared) | `create-adr` | ADR prefix generation from config |

### Skill Invocation Pattern

Skills invoke scripts using the `<skill-base-dir>` path provided at the top of the skill prompt:

```bash
# Skill-specific script
bash "<skill-base-dir>/scripts/install-framework.sh" "$(pwd)"

# Shared script (one directory up from skill base)
bash "<skill-base-dir>/../scripts/next-adr-number.sh" "<adrs-dir>" "<topic-slug>"
```

### Script Location Convention

- **Skill-specific scripts**: `<skill-name>/scripts/` — used by one skill only
- **Shared scripts**: `scripts/` (at skills root) — used by multiple skills

### Background

ADR-009 identified the need for deterministic operations but deferred implementation because it missed the highest-stakes candidate (framework installation with destructive `rm -rf`). The trigger condition — "bash command construction causes bugs" — was satisfied when the setup-architect skill repeatedly botched the installation copy path despite a prior fix (PR #5). This pattern was implemented to address both the installation failure and ADR numbering reliability.

---

## Input Validation & Sanitization

### Filename Sanitization Pattern
Expand Down Expand Up @@ -891,6 +953,12 @@ Don't extract when:

## Version History

**v1.3** (2026-02-10)
- Added script-based deterministic operations pattern (ADR-009 implementation)
- Documented when to script vs keep interpretive
- Added script interface convention and location convention
- Updated create-adr permission scope for script invocation

**v1.2** (2025-12-04)
- Added progressive disclosure pattern (ADR-008)
- Documented modular skill structure (SKILL.md + /references/ + /assets/)
Expand Down
19 changes: 14 additions & 5 deletions .claude/skills/create-adr/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: create-adr
description: Creates a NEW Architectural Decision Record (ADR) documenting a specific architectural decision. Use when the user requests "Create ADR for [topic]", "Document decision about [topic]", "Write ADR for [choice]", or when documenting technology choices, patterns, or architectural approaches. Do NOT use for reviews (use architecture-review or specialist-review), checking existing ADRs (use architecture-status), or general documentation.
allowed-tools: Read,Write,Bash(ls:*,grep:*)
allowed-tools: Read,Write,Bash(bash:*,ls:*,grep:*)
---

# Create Architectural Decision Record (ADR)
Expand All @@ -18,11 +18,20 @@ Ask if needed:
- What are the trade-offs?

### 2. Generate ADR Number

Use the deterministic numbering script to get the next ADR prefix. The script reads `numbering_format`, `sequential_format`, and `date_format` from `.architecture/config.yml` and returns the correct prefix.

```bash
# Find highest ADR number
ls .architecture/decisions/adrs/ | grep -E "^ADR-[0-9]+" | sed 's/ADR-//' | sed 's/-.*//' | sort -n | tail -1
bash "<skill-base-dir>/../scripts/next-adr-number.sh" ".architecture/decisions/adrs" "topic-slug"
```
New ADR = next sequential number (e.g., if highest is 003, create 004)

Where `<skill-base-dir>` is this skill's base directory shown at the top of the skill prompt, and `topic-slug` is the kebab-case topic from step 3.

- The script outputs the prefix on stdout (e.g., `001` for sequential, `20260210` for date-based)
- Exit code 2 means a collision was detected (ADR with same prefix and topic already exists) — alert the user
- Exit code 1 means a config error — check stderr for details

**Note**: Run step 3 (sanitize input) before step 2 so the topic-slug is available for collision detection. The numbering below is logical, not execution order.

### 3. Validate and Sanitize Input
**Security**: Sanitize user input to prevent path traversal and injection:
Expand All @@ -32,7 +41,7 @@ New ADR = next sequential number (e.g., if highest is 003, create 004)
- Validate result: ensure filename contains only [a-z0-9-]

### 4. Create Filename
Format: `ADR-XXX-kebab-case-title.md`
Format: `ADR-<prefix>-kebab-case-title.md` where `<prefix>` is the output from step 2.

Examples:
- `ADR-001-use-react-for-frontend.md`
Expand Down
124 changes: 124 additions & 0 deletions .claude/skills/scripts/next-adr-number.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
# next-adr-number.sh - Deterministic ADR prefix generation
#
# Reads numbering config from .architecture/config.yml and returns the
# next ADR prefix. Supports sequential (zero-padded) and date-based
# (strftime format) numbering.
#
# Usage: next-adr-number.sh <adrs-dir> [topic-slug]
#
# Arguments:
# adrs-dir Path to the ADRs directory (e.g., .architecture/decisions/adrs)
# topic-slug Optional kebab-case topic for collision detection
#
# Config options (in .architecture/config.yml under adr:):
# numbering_format: "sequential" (default) or "date-based"
# sequential_format: Zero-padding pattern, e.g., "000" for 3 digits (default: "000")
# date_format: strftime format string, e.g., "%Y%m%d" (default: "%Y%m%d")
#
# Exit codes:
# 0 Success - prefix written to stdout
# 1 Config error or bad arguments
# 2 Collision detected (existing ADR with same prefix and topic)

set -euo pipefail

ADRS_DIR="${1:?Usage: next-adr-number.sh <adrs-dir> [topic-slug]}"
TOPIC_SLUG="${2:-}"

# Validate ADRs directory exists
if [ ! -d "$ADRS_DIR" ]; then
echo "ERROR: ADR directory not found: $ADRS_DIR" >&2
exit 1
fi

# Find config.yml relative to adrs-dir
# Expected: .architecture/decisions/adrs -> .architecture/config.yml
CONFIG_FILE=""
candidate="$(cd "$ADRS_DIR" && pwd)"
# Walk up looking for config.yml alongside a decisions/ dir
for _ in 1 2 3 4; do
candidate="$(dirname "$candidate")"
if [ -f "$candidate/config.yml" ] && [ -d "$candidate/decisions" ]; then
CONFIG_FILE="$candidate/config.yml"
break
fi
done

# --- Read config values ---
# Simple grep-based extraction — avoids YAML parser dependency

get_config_value() {
local key="$1"
local default="$2"
if [ -n "$CONFIG_FILE" ] && [ -f "$CONFIG_FILE" ]; then
local value
value=$(grep -E "^\s*${key}:" "$CONFIG_FILE" 2>/dev/null | head -1 | sed 's/^[^:]*:\s*//' | sed 's/\s*#.*//' | sed 's/^["'"'"']//' | sed 's/["'"'"']$//' | tr -d '[:space:]')
if [ -n "$value" ]; then
echo "$value"
return
fi
fi
echo "$default"
}

NUMBERING_FORMAT=$(get_config_value "numbering_format" "sequential")
SEQUENTIAL_FORMAT=$(get_config_value "sequential_format" "000")
DATE_FORMAT=$(get_config_value "date_format" "%Y%m%d")

# --- Generate prefix ---

generate_sequential_prefix() {
local format="$1"

# Count zeros to determine padding width
local width=${#format}
if [ "$width" -lt 1 ]; then
width=3
fi

# Find highest existing ADR number
local highest=0
if ls "$ADRS_DIR"/ADR-* 1>/dev/null 2>&1; then
highest=$(ls -1 "$ADRS_DIR" | grep -Eo "^ADR-[0-9]+" | sed 's/ADR-//' | sort -n | tail -1)
# Strip leading zeros for arithmetic
highest=$((10#${highest}))
fi

local next=$((highest + 1))
printf "%0${width}d" "$next"
}

generate_date_prefix() {
local fmt="$1"
date +"$fmt"
}

# --- Main ---

case "$NUMBERING_FORMAT" in
sequential)
PREFIX=$(generate_sequential_prefix "$SEQUENTIAL_FORMAT")
;;
date-based|date_based|datebased)
PREFIX=$(generate_date_prefix "$DATE_FORMAT")
;;
*)
echo "ERROR: Unknown numbering_format: $NUMBERING_FORMAT (expected: sequential or date-based)" >&2
exit 1
;;
esac

# --- Collision detection ---

if [ -n "$TOPIC_SLUG" ]; then
# Check for existing file with same prefix and topic
if ls "$ADRS_DIR"/ADR-"${PREFIX}"-"${TOPIC_SLUG}"* 1>/dev/null 2>&1; then
existing=$(ls -1 "$ADRS_DIR"/ADR-"${PREFIX}"-"${TOPIC_SLUG}"* 2>/dev/null | head -1)
echo "ERROR: ADR already exists: $(basename "$existing")" >&2
echo "ERROR: This is likely a duplicate — review before creating another" >&2
exit 2
fi
fi

echo "$PREFIX"
Loading