Skip to content

Commit 18987eb

Browse files
committed
feat(codex): add SessionStart hook for Generated-with commit trailers
Adds `assistantkit codex hooks generated-with`, which reads Codex SessionStart hook JSON from stdin and emits guidance instructing Codex to include a `Generated-with: OpenAI Codex <model>` trailer using the active model from the hook input.
1 parent e739f38 commit 18987eb

3 files changed

Lines changed: 180 additions & 0 deletions

File tree

cmd/assistantkit/codex.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package main
2+
3+
import (
4+
"os"
5+
6+
codexhooks "github.com/plexusone/assistantkit/hooks/codex"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var (
11+
codexGeneratedWithAssistantName string
12+
codexGeneratedWithUnknownModel string
13+
)
14+
15+
var codexCmd = &cobra.Command{
16+
Use: "codex",
17+
Short: "Utilities for OpenAI Codex",
18+
}
19+
20+
var codexHooksCmd = &cobra.Command{
21+
Use: "hooks",
22+
Short: "Codex hook commands",
23+
}
24+
25+
var codexHooksGeneratedWithCmd = &cobra.Command{
26+
Use: "generated-with",
27+
Short: "Emit SessionStart context for Generated-with commit trailers",
28+
Long: `Read Codex hook JSON from stdin and emit SessionStart hook output that
29+
instructs Codex to include a Generated-with trailer containing the active model.
30+
31+
Example:
32+
assistantkit codex hooks generated-with`,
33+
RunE: runCodexHooksGeneratedWith,
34+
}
35+
36+
func init() {
37+
rootCmd.AddCommand(codexCmd)
38+
codexCmd.AddCommand(codexHooksCmd)
39+
codexHooksCmd.AddCommand(codexHooksGeneratedWithCmd)
40+
41+
codexHooksGeneratedWithCmd.Flags().StringVar(&codexGeneratedWithAssistantName, "assistant-name", codexhooks.DefaultAssistantName, "Assistant name for the Generated-with trailer")
42+
codexHooksGeneratedWithCmd.Flags().StringVar(&codexGeneratedWithUnknownModel, "unknown-model", codexhooks.DefaultUnknownModel, "Model name to use when Codex hook input omits model")
43+
}
44+
45+
func runCodexHooksGeneratedWith(cmd *cobra.Command, args []string) error {
46+
return codexhooks.RunGeneratedWith(os.Stdin, os.Stdout, codexGeneratedWithAssistantName, codexGeneratedWithUnknownModel)
47+
}

hooks/codex/generated_with.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Package codex provides helpers for OpenAI Codex hook commands.
2+
package codex
3+
4+
import (
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
)
9+
10+
const (
11+
// DefaultAssistantName is the product name used in generated commit trailers.
12+
DefaultAssistantName = "OpenAI Codex"
13+
14+
// DefaultUnknownModel is used when Codex hook input omits the active model.
15+
DefaultUnknownModel = "unknown-model"
16+
)
17+
18+
// HookInput is the subset of Codex hook input used by the generated-with hook.
19+
type HookInput struct {
20+
HookEventName string `json:"hook_event_name,omitempty"`
21+
Model string `json:"model,omitempty"`
22+
}
23+
24+
// HookOutput is the Codex hook output shape for adding developer context.
25+
type HookOutput struct {
26+
HookSpecificOutput HookSpecificOutput `json:"hookSpecificOutput"`
27+
}
28+
29+
// HookSpecificOutput is the Codex hook-specific output payload.
30+
type HookSpecificOutput struct {
31+
HookEventName string `json:"hookEventName"`
32+
AdditionalContext string `json:"additionalContext"`
33+
}
34+
35+
// GeneratedWithTrailer returns the commit trailer for the given model.
36+
func GeneratedWithTrailer(assistantName, model string) string {
37+
if assistantName == "" {
38+
assistantName = DefaultAssistantName
39+
}
40+
if model == "" {
41+
model = DefaultUnknownModel
42+
}
43+
return fmt.Sprintf("Generated-with: %s %s", assistantName, model)
44+
}
45+
46+
// GeneratedWithContext returns developer context that asks Codex to include
47+
// detailed commit messages and a model-specific Generated-with trailer.
48+
func GeneratedWithContext(assistantName, model string) string {
49+
return fmt.Sprintf(
50+
"When creating or amending commits, use a detailed Conventional Commit message with an explanatory body and a Tests section when tests were run. Include this trailer when the active model is known:\n\n%s",
51+
GeneratedWithTrailer(assistantName, model),
52+
)
53+
}
54+
55+
// NewGeneratedWithOutput builds the SessionStart hook output for the model.
56+
func NewGeneratedWithOutput(assistantName, model string) HookOutput {
57+
return HookOutput{
58+
HookSpecificOutput: HookSpecificOutput{
59+
HookEventName: "SessionStart",
60+
AdditionalContext: GeneratedWithContext(assistantName, model),
61+
},
62+
}
63+
}
64+
65+
// RunGeneratedWith reads Codex hook input from r and writes Codex hook output to w.
66+
func RunGeneratedWith(r io.Reader, w io.Writer, assistantName, unknownModel string) error {
67+
if unknownModel == "" {
68+
unknownModel = DefaultUnknownModel
69+
}
70+
71+
var input HookInput
72+
if err := json.NewDecoder(r).Decode(&input); err != nil {
73+
return fmt.Errorf("decode codex hook input: %w", err)
74+
}
75+
76+
model := input.Model
77+
if model == "" {
78+
model = unknownModel
79+
}
80+
81+
enc := json.NewEncoder(w)
82+
enc.SetIndent("", " ")
83+
if err := enc.Encode(NewGeneratedWithOutput(assistantName, model)); err != nil {
84+
return fmt.Errorf("encode codex hook output: %w", err)
85+
}
86+
return nil
87+
}

hooks/codex/generated_with_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package codex
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestGeneratedWithTrailer(t *testing.T) {
11+
got := GeneratedWithTrailer("OpenAI Codex", "gpt-5.5")
12+
want := "Generated-with: OpenAI Codex gpt-5.5"
13+
if got != want {
14+
t.Fatalf("GeneratedWithTrailer() = %q, want %q", got, want)
15+
}
16+
}
17+
18+
func TestRunGeneratedWith(t *testing.T) {
19+
var out bytes.Buffer
20+
err := RunGeneratedWith(strings.NewReader(`{"hook_event_name":"SessionStart","model":"gpt-5.6-sol"}`), &out, "OpenAI Codex", "")
21+
if err != nil {
22+
t.Fatalf("RunGeneratedWith() error = %v", err)
23+
}
24+
25+
var got HookOutput
26+
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
27+
t.Fatalf("output is not valid JSON: %v", err)
28+
}
29+
if got.HookSpecificOutput.HookEventName != "SessionStart" {
30+
t.Fatalf("hookEventName = %q, want SessionStart", got.HookSpecificOutput.HookEventName)
31+
}
32+
if !strings.Contains(got.HookSpecificOutput.AdditionalContext, "Generated-with: OpenAI Codex gpt-5.6-sol") {
33+
t.Fatalf("additionalContext missing generated-with trailer: %q", got.HookSpecificOutput.AdditionalContext)
34+
}
35+
}
36+
37+
func TestRunGeneratedWith_UnknownModelFallback(t *testing.T) {
38+
var out bytes.Buffer
39+
err := RunGeneratedWith(strings.NewReader(`{"hook_event_name":"SessionStart"}`), &out, "OpenAI Codex", "unknown")
40+
if err != nil {
41+
t.Fatalf("RunGeneratedWith() error = %v", err)
42+
}
43+
if !strings.Contains(out.String(), "Generated-with: OpenAI Codex unknown") {
44+
t.Fatalf("output missing unknown model fallback: %s", out.String())
45+
}
46+
}

0 commit comments

Comments
 (0)