Skip to content

Commit 9c51f6d

Browse files
authored
Merge pull request #67 from syou6162/pre_compact
feat: PreCompact JSON出力対応
2 parents d02d609 + e38141d commit 9c51f6d

File tree

11 files changed

+934
-71
lines changed

11 files changed

+934
-71
lines changed

CLAUDE.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,68 @@ Notification:
702702
command: "get-task-status.sh" # Returns JSON with additionalContext
703703
```
704704

705+
### PreCompact JSON Output
706+
707+
PreCompact hooks support JSON output format for Claude Code integration. Actions can return structured output for pre-compaction processing:
708+
709+
**Output Action** (type: `output`):
710+
```yaml
711+
PreCompact:
712+
- matcher: "manual" # optional: "manual" or "auto"
713+
actions:
714+
- type: output
715+
message: "Pre-compaction processing message"
716+
```
717+
718+
**Command Action** (type: `command`):
719+
Commands must output JSON with the following structure:
720+
```json
721+
{
722+
"continue": true,
723+
"stopReason": "Optional stop reason",
724+
"suppressOutput": false,
725+
"systemMessage": "Optional system message"
726+
}
727+
```
728+
729+
**Important**: PreCompact uses **Common JSON Fields only**. Unlike other events, it has no `decision`, `reason`, or `hookSpecificOutput` fields. PreCompact is a pre-processing hook and **cannot block** compaction.
730+
731+
**Matcher Support**:
732+
PreCompact hooks support a `matcher` field to filter by trigger type:
733+
- `"manual"`: Matches manual compaction triggers
734+
- `"auto"`: Matches automatic compaction triggers
735+
- Empty/omitted: Matches all triggers
736+
737+
**Field Merging**:
738+
When multiple actions execute:
739+
- `continue`: Always `true` (compaction cannot be blocked)
740+
- `systemMessage`: Concatenated with newline separator
741+
- `stopReason` and `suppressOutput`: Last value wins
742+
743+
**Exit Code Behavior**:
744+
PreCompact hooks **always exit with code 0**. The `continue` field is always `true` because compaction cannot be blocked.
745+
746+
Errors are logged to stderr as warnings, but cchook continues to output JSON and exits successfully. This ensures pre-compaction processing completes even on errors.
747+
748+
**Message Mapping**:
749+
Output action `message` field is mapped to `systemMessage` (shown to user, not to Claude). This maintains backward compatibility with the old stdout-based implementation.
750+
751+
**Example**:
752+
```yaml
753+
PreCompact:
754+
- matcher: "manual"
755+
actions:
756+
- type: output
757+
message: "Manual compaction triggered - preparing context"
758+
- matcher: "auto"
759+
actions:
760+
- type: command
761+
command: "prepare-auto-compaction.sh" # Returns JSON with systemMessage
762+
- actions:
763+
- type: output
764+
message: "General pre-compaction cleanup"
765+
```
766+
705767
### SessionEnd JSON Output
706768

707769
SessionEnd hooks support JSON output format for Claude Code integration. Actions can return structured output for session cleanup:

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -627,18 +627,18 @@ All conditions return proper error messages for unknown condition types, ensurin
627627
- Print message
628628
- Default `exit_status`:
629629
- 0 for SessionStart, UserPromptSubmit (non-blocking events)
630-
- 2 for Notification, PreCompact
631-
- Note: PreToolUse, Stop, PostToolUse, and SubagentStop use JSON output (exit_status ignored)
630+
- 2 for Notification (legacy)
631+
- Note: Most events use JSON output (exit_status ignored). See "JSON Output Events" below.
632632

633633
### Exit Status Control
634634

635-
**JSON Output Events** (SessionStart, UserPromptSubmit, PreToolUse, Stop, SubagentStop, PostToolUse, SessionEnd):
635+
**JSON Output Events** (SessionStart, UserPromptSubmit, PreToolUse, Stop, SubagentStop, PostToolUse, PreCompact, SessionEnd, Notification):
636636
- Always exit with code 0
637637
- Control behavior via JSON fields (`decision`, `permissionDecision`, etc.)
638638
- Errors logged to stderr as warnings
639639
- See CLAUDE.md for detailed JSON output format
640640

641-
**Legacy Exit Code Events** (Notification, PreCompact):
641+
**Legacy Exit Code Events** (Notification only):
642642
- 0
643643
- Success, allow execution, output to stdout
644644
- 2
@@ -653,6 +653,13 @@ All conditions return proper error messages for unknown condition types, ensurin
653653
- After JSON migration, use `decision` field: omit for allow, `"block"` for deny
654654
- `exit_status` field is ignored in JSON mode (stderr warning emitted)
655655

656+
**Migration Note** (PreCompact):
657+
- Prior to JSON support, PreCompact used exit codes (default `exit_status: 2`)
658+
- After JSON migration, PreCompact uses Common JSON Fields only (no `decision` field)
659+
- PreCompact always returns `continue: true` (compaction cannot be blocked)
660+
- `exit_status` field is ignored in JSON mode (stderr warning emitted)
661+
- Matcher support added: `matcher: "manual"` or `"auto"` to filter by trigger type
662+
656663
### Template Syntax
657664

658665
Access JSON data using `{.field}` syntax with full jq query support:

executor.go

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -466,17 +466,84 @@ func (e *ActionExecutor) ExecuteSubagentStopAction(action Action, input *Subagen
466466

467467
// ExecutePreCompactAction executes an action for the PreCompact event.
468468
// Supports command execution and output actions.
469-
func (e *ActionExecutor) ExecutePreCompactAction(action Action, input *PreCompactInput, rawJSON interface{}) error {
469+
// ExecutePreCompactAction executes an action for the PreCompact event and returns ActionOutput.
470+
// PreCompact always returns continue=true (fail-safe: compaction cannot be blocked).
471+
// Errors are reported via systemMessage field, not by blocking execution.
472+
func (e *ActionExecutor) ExecutePreCompactAction(action Action, input *PreCompactInput, rawJSON interface{}) (*ActionOutput, error) {
470473
switch action.Type {
471474
case "command":
472475
cmd := unifiedTemplateReplace(action.Command, rawJSON)
473-
if err := e.runner.RunCommand(cmd, action.UseStdin, rawJSON); err != nil {
474-
return err
476+
stdout, stderr, exitCode, err := e.runner.RunCommandWithOutput(cmd, action.UseStdin, rawJSON)
477+
478+
// Command failed with non-zero exit code
479+
if exitCode != 0 {
480+
errMsg := fmt.Sprintf("Command failed with exit code %d: %s", exitCode, stderr)
481+
if strings.TrimSpace(stderr) == "" && err != nil {
482+
errMsg = fmt.Sprintf("Command failed with exit code %d: %v", exitCode, err)
483+
}
484+
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
485+
return &ActionOutput{
486+
Continue: true,
487+
SystemMessage: errMsg,
488+
}, nil
489+
}
490+
491+
// Empty stdout - Allow compaction (validation-type CLI tools: silence = OK)
492+
if strings.TrimSpace(stdout) == "" {
493+
return &ActionOutput{
494+
Continue: true,
495+
}, nil
475496
}
497+
498+
// Parse JSON output (PreCompactOutput has Common JSON Fields only)
499+
var cmdOutput PreCompactOutput
500+
if err := json.Unmarshal([]byte(stdout), &cmdOutput); err != nil {
501+
errMsg := fmt.Sprintf("Command output is not valid JSON: %s", stdout)
502+
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
503+
return &ActionOutput{
504+
Continue: true,
505+
SystemMessage: errMsg,
506+
}, nil
507+
}
508+
509+
// Check for unsupported fields and log warnings to stderr
510+
checkUnsupportedFieldsPreCompact(stdout)
511+
512+
// Build ActionOutput from parsed JSON
513+
return &ActionOutput{
514+
Continue: true,
515+
StopReason: cmdOutput.StopReason,
516+
SuppressOutput: cmdOutput.SuppressOutput,
517+
SystemMessage: cmdOutput.SystemMessage,
518+
}, nil
519+
476520
case "output":
477-
return handleOutput(action.Message, action.ExitStatus, rawJSON)
521+
processedMessage := unifiedTemplateReplace(action.Message, rawJSON)
522+
523+
// Emit warning if exit_status is set (backward compatibility - no longer used)
524+
if action.ExitStatus != nil {
525+
fmt.Fprintf(os.Stderr, "Warning: exit_status field is ignored in PreCompact output actions (JSON output does not use exit codes)\n")
526+
}
527+
528+
// Empty message is an error (fail-safe)
529+
if strings.TrimSpace(processedMessage) == "" {
530+
errMsg := "Empty message in PreCompact action"
531+
fmt.Fprintf(os.Stderr, "Warning: %s\n", errMsg)
532+
return &ActionOutput{
533+
Continue: true,
534+
SystemMessage: errMsg,
535+
}, nil
536+
}
537+
538+
// Output action: message maps to systemMessage
539+
return &ActionOutput{
540+
Continue: true,
541+
SystemMessage: processedMessage,
542+
}, nil
543+
544+
default:
545+
return nil, fmt.Errorf("unknown action type: %s", action.Type)
478546
}
479-
return nil
480547
}
481548

482549
// ExecuteSessionStartAction executes an action for the SessionStart event.
@@ -1241,6 +1308,28 @@ func checkUnsupportedFieldsSessionEnd(stdout string) {
12411308
}
12421309
}
12431310

1311+
//nolint:unused // Will be used in Step 3
1312+
func checkUnsupportedFieldsPreCompact(stdout string) {
1313+
var data map[string]interface{}
1314+
if err := json.Unmarshal([]byte(stdout), &data); err != nil {
1315+
return
1316+
}
1317+
1318+
supportedFields := map[string]bool{
1319+
"continue": true,
1320+
"stopReason": true,
1321+
"suppressOutput": true,
1322+
"systemMessage": true,
1323+
// Note: decision, reason, hookSpecificOutput are NOT supported for PreCompact hooks
1324+
}
1325+
1326+
for field := range data {
1327+
if !supportedFields[field] {
1328+
fmt.Fprintf(os.Stderr, "Warning: Field '%s' is not supported for PreCompact hooks\n", field)
1329+
}
1330+
}
1331+
}
1332+
12441333
// checkUnsupportedFieldsNotification checks for unsupported fields in Notification JSON output
12451334
// and logs warnings to stderr for any fields that are not in the supported list.
12461335
func checkUnsupportedFieldsNotification(stdout string) {

0 commit comments

Comments
 (0)