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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .speckit/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ git push origin v1.0.0
- `logging/`: JSON Lines logging infrastructure
- `cli/`: Command-line interface (init, install, debug, validate, logs, explain)

### Claude Code JSON Protocol
CCH communicates with Claude Code via stdin/stdout JSON. Key field mappings:
- **Event field**: Claude Code sends `hook_event_name` (CCH accepts both `hook_event_name` and `event_type` via serde alias)
- **Response field**: CCH serializes `continue` (not `continue_`) via `#[serde(rename = "continue")]`
- **Timestamp**: Claude Code may not send `timestamp`; CCH defaults to `Utc::now()`
- **Event types**: PreToolUse, PostToolUse, Stop, SessionStart, SessionEnd, PostToolUseFailure, SubagentStart, SubagentStop, Notification, Setup, PermissionRequest, UserPromptSubmit, PreCompact

### Key Patterns
- Async-first design for performance
- Configuration-driven behavior (no hardcoded rules)
Expand Down Expand Up @@ -232,4 +239,25 @@ git push origin v1.0.0
- YAML configuration file loading
- External script execution (Python validators)
- JSON Lines log file management
- Directory-based context file injection
- Directory-based context file injection

---

## cch-advanced-rules (Backlog)
**Status**: Backlog
**Priority**: P3 (Future Enhancement)
**Description**: Advanced rule features removed from skill docs during schema fix — never implemented in CCH binary
**Location**: cch_cli/ (future Rust implementation)
**Completion**: 0% - Spec only

### SDD Artifacts
- **Spec:** `.speckit/features/cch-advanced-rules/spec.md`

### User Stories (Backlog)
- [ ] US-ADV-01 (P2): `enabled_when` conditional matcher
- [ ] US-ADV-02 (P3): `prompt_match` regex matcher
- [ ] US-ADV-03 (P3): `require_fields` action type
- [ ] US-ADV-04 (P2): Inline content injection (`inject_inline`)
- [ ] US-ADV-05 (P2): Command-based context generation (`inject_command`)
- [ ] US-ADV-06 (P3): Inline script blocks in `run:`
- [ ] US-ADV-07 (P3): Context variables for expressions
146 changes: 146 additions & 0 deletions .speckit/features/cch-advanced-rules/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Feature Specification: CCH Advanced Rules

**Feature Branch**: `feature/cch-advanced-rules`
**Created**: 2026-01-27
**Status**: Backlog
**Input**: Features removed from mastering-hooks skill docs (never implemented in CCH binary)

## Background

During the skill documentation fix (January 2026), several features documented in the mastering-hooks skill were found to not exist in the CCH binary. These were fabricated by the AI that generated the original skill docs. This spec captures them as future backlog items.

## User Scenarios & Testing

### User Story 1 - enabled_when Conditional Matcher (Priority: P2)

Users want rules that only activate under certain conditions (e.g., CI environment, specific branches, test files). Currently, all rules are always active when their matchers match.

**Why this priority**: Enables environment-aware rules without duplicating configs. High value for teams with different dev/CI workflows.

**Independent Test**: Can be tested by creating a rule with `enabled_when: "env.CI == 'true'"` and verifying it only fires when the CI env var is set.

**Acceptance Scenarios**:

1. **Given** a rule with `enabled_when: "env.CI == 'true'"`, **When** the rule is evaluated in a non-CI environment, **Then** the rule does not match
2. **Given** a rule with `enabled_when: "env.CI == 'true'"`, **When** the rule is evaluated with `CI=true`, **Then** the rule matches normally

---

### User Story 2 - prompt_match Matcher (Priority: P3)

Users want rules that match against user prompt text, enabling prompt-based routing (e.g., deploy requests, slash commands).

**Why this priority**: Useful but niche. Most rules match on tool usage, not prompt text.

**Independent Test**: Can be tested by creating a rule with `prompt_match: "(?i)deploy"` and simulating a UserPromptSubmit event.

**Acceptance Scenarios**:

1. **Given** a rule with `prompt_match: "(?i)deploy"`, **When** a user types "Deploy to production", **Then** the rule matches
2. **Given** a rule with `prompt_match: "^/fix"`, **When** a user types "Fix the bug", **Then** the rule does not match (no leading slash)

---

### User Story 3 - require_fields Action (Priority: P3)

Users want to validate that required fields exist in tool input before allowing execution.

**Why this priority**: Low priority — most validation can be done via `run:` scripts.

**Independent Test**: Can be tested by creating a rule with `require_fields: [path, content]` on the Write tool.

**Acceptance Scenarios**:

1. **Given** a rule requiring fields `[path, content]` on Write, **When** Write is called with both, **Then** the tool proceeds
2. **Given** a rule requiring fields `[path, content]` on Write, **When** Write is called without `content`, **Then** the tool is blocked

---

### User Story 4 - Inline Content Injection (Priority: P2)

Users want to inject short markdown content directly in the rule without creating a separate file.

**Why this priority**: Reduces file proliferation for simple warnings or reminders. Currently `inject:` only accepts file paths.

**Independent Test**: Can be tested by creating a rule with `inject_inline: "Warning: check before proceeding"` and verifying the content appears in context.

**Acceptance Scenarios**:

1. **Given** a rule with `inject_inline: "## Warning\nBe careful"`, **When** the rule matches, **Then** the inline content is injected into Claude's context

---

### User Story 5 - Command-Based Context Generation (Priority: P2)

Users want to generate context dynamically by running a shell command (e.g., `git branch --show-current`).

**Why this priority**: Enables dynamic context without full validator scripts. Currently requires a `run:` script that outputs JSON.

**Independent Test**: Can be tested by creating a rule with `inject_command: "git branch --show-current"` and verifying the output appears in context.

**Acceptance Scenarios**:

1. **Given** a rule with `inject_command: "echo '## Branch\nMain'"`, **When** the rule matches, **Then** the command output is injected as context

---

### User Story 6 - Inline Script Blocks in run: (Priority: P3)

Users want to write small validator scripts directly in hooks.yaml instead of creating separate script files.

**Why this priority**: Convenience for simple checks. Currently `run:` only accepts file paths.

**Independent Test**: Can be tested by creating a rule with multiline `run:` script and verifying execution.

**Acceptance Scenarios**:

1. **Given** a rule with `run: |` multiline script block, **When** the rule matches, **Then** the inline script executes and returns JSON

---

### User Story 7 - Context Variables in Expressions (Priority: P3)

Users want access to runtime variables (`tool.name`, `env.CI`, `session.id`) in `enabled_when` expressions.

**Why this priority**: Required by US-ADV-01 (enabled_when). Dependency for conditional matching.

**Independent Test**: Can be tested by creating rules referencing `tool.name`, `env.CI`, etc.

**Acceptance Scenarios**:

1. **Given** an expression `tool.name == 'Bash'`, **When** Bash tool is used, **Then** the expression evaluates to true
2. **Given** an expression `env.USER == 'ci-bot'`, **When** USER env var is "ci-bot", **Then** the expression evaluates to true

---

### Edge Cases

- What happens when `enabled_when` expression has a syntax error?
- How does `inject_command` handle script timeouts?
- What happens with `require_fields` on tools that have no input fields?

## Requirements

### Functional Requirements

- **FR-001**: System MUST support `enabled_when` conditional expressions on rules
- **FR-002**: System MUST support `prompt_match` regex matching on user prompts
- **FR-003**: System MUST support `require_fields` action type for input validation
- **FR-004**: System MUST support inline content injection (not just file paths)
- **FR-005**: System MUST support command-based context generation
- **FR-006**: System MUST support inline script blocks in `run:` action
- **FR-007**: System MUST provide context variables for expressions

### Key Entities

- **Expression**: A conditional expression evaluated at runtime (used by `enabled_when`)
- **ContextVariable**: A runtime variable providing event context (tool.name, env.CI, etc.)

## Success Criteria

### Measurable Outcomes

- **SC-001**: All 7 user stories have passing integration tests
- **SC-002**: Backward compatibility maintained — existing configs work without changes
- **SC-003**: Performance stays under 10ms for rule evaluation with new matchers
- **SC-004**: `cch validate` catches expression syntax errors
10 changes: 5 additions & 5 deletions .speckit/features/enhanced-logging/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ impl EventDetails {
.map(String::from);
EventDetails::Grep { pattern, path }
}
None if matches!(event.event_type, EventType::SessionStart | EventType::SessionEnd) => {
None if matches!(event.hook_event_name, EventType::SessionStart | EventType::SessionEnd) => {
let source = tool_input
.and_then(|ti| ti.get("source"))
.and_then(|s| s.as_str())
Expand Down Expand Up @@ -380,7 +380,7 @@ pub async fn process_event(event: Event, debug_config: &DebugConfig) -> Result<R
// Log the event with enhanced fields
let entry = LogEntry {
timestamp: event.timestamp,
event_type: format!("{:?}", event.event_type),
event_type: format!("{:?}", event.hook_event_name),
session_id: event.session_id.clone(),
tool_name: event.tool_name.clone(),
rules_matched: matched_rules.into_iter().map(|r| r.name.clone()).collect(),
Expand Down Expand Up @@ -472,7 +472,7 @@ mod event_details_tests {
#[test]
fn test_extract_bash_event() {
let event = Event {
event_type: EventType::PreToolUse,
hook_event_name: EventType::PreToolUse,
tool_name: Some("Bash".to_string()),
tool_input: Some(serde_json::json!({
"command": "git push --force"
Expand All @@ -489,7 +489,7 @@ mod event_details_tests {
#[test]
fn test_extract_unknown_tool() {
let event = Event {
event_type: EventType::PreToolUse,
hook_event_name: EventType::PreToolUse,
tool_name: Some("FutureTool".to_string()),
tool_input: None,
session_id: "test".to_string(),
Expand Down Expand Up @@ -546,7 +546,7 @@ After implementation, verify:

1. **Normal Mode:**
```bash
echo '{"event_type":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git status"},...}' | cch
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git status"},"session_id":"test"}' | cch
# Check ~/.claude/logs/cch.log contains event_details and response
```

Expand Down
13 changes: 7 additions & 6 deletions .speckit/features/enhanced-logging/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Enhance CCH logging to capture detailed event context, response summaries, and p
**So that** I can correlate logs with actual Claude behavior

**Acceptance Criteria:**
- [ ] Log entries include `continue_` boolean
- [ ] Log entries include `continue` boolean (serialized via `#[serde(rename = "continue")]`)
- [ ] Blocked responses include `reason` field
- [ ] Injection responses include `context_length` (not full content)
- [ ] Response summary is always present on processed events
Expand Down Expand Up @@ -100,6 +100,7 @@ pub enum EventDetails {
```rust
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ResponseSummary {
#[serde(rename = "continue")]
pub continue_: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
Expand Down Expand Up @@ -138,7 +139,7 @@ pub struct MatcherResults {
pub struct LogEntry {
// === Existing fields (preserved) ===
pub timestamp: DateTime<Utc>,
pub event_type: String,
pub hook_event_name: String, // Note: aliased from event_type for backward compat
pub session_id: String,
pub tool_name: Option<String>,
pub rules_matched: Vec<String>,
Expand Down Expand Up @@ -246,7 +247,7 @@ pub struct LogEntry {
```json
{
"timestamp": "2026-01-22T14:32:11Z",
"event_type": "PreToolUse",
"hook_event_name": "PreToolUse",
"session_id": "abc123",
"tool_name": "Bash",
"event_details": {
Expand All @@ -256,7 +257,7 @@ pub struct LogEntry {
"rules_matched": ["block-force-push"],
"outcome": "block",
"response": {
"continue_": false,
"continue": false,
"reason": "Blocked by rule 'block-force-push': Force push is not allowed"
},
"timing": {
Expand All @@ -270,7 +271,7 @@ pub struct LogEntry {
```json
{
"timestamp": "2026-01-22T14:32:11Z",
"event_type": "PreToolUse",
"hook_event_name": "PreToolUse",
"session_id": "abc123",
"tool_name": "Bash",
"event_details": {
Expand All @@ -280,7 +281,7 @@ pub struct LogEntry {
"rules_matched": ["block-force-push"],
"outcome": "block",
"response": {
"continue_": false,
"continue": false,
"reason": "Blocked by rule 'block-force-push'"
},
"timing": {
Expand Down
2 changes: 1 addition & 1 deletion .speckit/features/enhanced-logging/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
- [x] Session variant with `source`, `reason`, `transcript_path`, `cwd` fields
- [x] Permission variant with `permission_mode` and boxed `tool_details`
- [x] Unknown variant with `tool_name` field
- [x] Add `ResponseSummary` struct with `continue_`, `reason`, `context_length`
- [x] Add `ResponseSummary` struct with `continue` (serde-renamed from `continue_`), `reason`, `context_length`
- [x] Add `RuleEvaluation` struct with `rule_name`, `matched`, `matcher_results`
- [x] Add `MatcherResults` struct with individual matcher result fields
- [x] Add `DebugConfig` struct with `enabled` flag
Expand Down
2 changes: 1 addition & 1 deletion .speckit/features/integration-testing/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ EOF
fn test_oq_block_force_push() {
let temp = setup_test_workspace("block-force-push");
let event = json!({
"event_type": "PreToolUse",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {"command": "git push --force origin main"},
"session_id": "test-001"
Expand Down
2 changes: 1 addition & 1 deletion .speckit/features/phase2-governance/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ pub enum Decision {
**Output Format:**
```
Rule: <name>
Event: <event_type>
Event: <hook_event_name>
Mode: <mode> (default: enforce)
Priority: <priority> (default: 0)

Expand Down
4 changes: 2 additions & 2 deletions .speckit/features/rulez-ui/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,13 +496,13 @@ Enable testing rules by simulating events through CCH binary.
```rust
#[tauri::command]
pub async fn run_debug(
event_type: String,
hook_event_name: String,
tool: Option<String>,
command: Option<String>,
path: Option<String>,
) -> Result<DebugResult, String> {
let mut cmd = Command::new("cch");
cmd.arg("debug").arg(&event_type);
cmd.arg("debug").arg(&hook_event_name);

if let Some(t) = tool {
cmd.arg("--tool").arg(t);
Expand Down
2 changes: 1 addition & 1 deletion .speckit/features/rulez-ui/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ rulez_ui/
| `list_config_files` | List global and project configs | `project_dir?: string` |
| `read_config` | Read config file content | `path: string` |
| `write_config` | Write config file content | `path: string, content: string` |
| `run_debug` | Execute CCH debug command | `event_type, tool?, command?, path?` |
| `run_debug` | Execute CCH debug command | `hook_event_name, tool?, command?, path?` |
| `validate_config` | Validate config via CCH | `path: string` |

---
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ members = ["cch_cli"]
resolver = "2"

[workspace.package]
version = "1.0.0"
authors = ["Spillwave Solutions <engineering@spillwave.com>"]
version = "1.0.2"
authors = ["Rick Hightower <rick@spillwave.com>"]
license = "MIT OR Apache-2.0"
edition = "2024"

Expand Down
6 changes: 5 additions & 1 deletion cch_cli/src/cli/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,16 @@ fn build_event(
};

Event {
event_type: event_type.as_model_event_type(),
hook_event_name: event_type.as_model_event_type(),
session_id,
tool_name: Some(tool_name),
tool_input: Some(tool_input),
timestamp: Utc::now(),
user_id: None,
transcript_path: None,
cwd: None,
permission_mode: None,
tool_use_id: None,
}
}

Expand Down
Loading