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
49 changes: 26 additions & 23 deletions internal/tool/api_compat.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,13 @@ type APICompatTool struct {
checker *CompatChecker
}

// APICompatInput is the typed input for APICompatTool.
type APICompatInput struct {
PackagePath string `json:"package_path"`
BaselinePath string `json:"baseline_path"`
SaveBaseline bool `json:"save_baseline"`
}

// NewAPICompatTool creates a new APICompatTool.
func NewAPICompatTool() *APICompatTool {
return &APICompatTool{
Expand All @@ -646,34 +653,30 @@ func (t *APICompatTool) Description() string {
}

func (t *APICompatTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"package_path": map[string]interface{}{
"type": "string",
"description": "Path to the Go package directory to analyze",
},
"baseline_path": map[string]interface{}{
"type": "string",
"description": "Path to the baseline snapshot JSON file",
},
"save_baseline": map[string]interface{}{
"type": "boolean",
"description": "If true, save current API as baseline instead of comparing",
},
return apiCompatSchema.ToJSONSchema()
}

// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (t APICompatTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"package_path": {Type: "string", Description: "Path to the Go package directory to analyze"},
"baseline_path": {Type: "string", Description: "Path to the baseline snapshot JSON file"},
"save_baseline": {Type: "boolean", Description: "If true, save current API as baseline instead of comparing"},
},
"required": []string{"package_path"},
Required: []string{"package_path"},
}
}

// apiCompatSchema is the single source of truth for APICompat's input schema.
var apiCompatSchema = APICompatTool{}.Schema()

func (t *APICompatTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var params struct {
PackagePath string `json:"package_path"`
BaselinePath string `json:"baseline_path"`
SaveBaseline bool `json:"save_baseline"`
}
if err := json.Unmarshal(input, &params); err != nil {
return "", fmt.Errorf("parsing parameters: %w", err)
params, err := DecodeInput[APICompatInput]("APICompat", input)
if err != nil {
return "", err
}

if params.PackagePath == "" {
Expand Down
54 changes: 27 additions & 27 deletions internal/tool/app_verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,41 @@ import (
// by covering the part that actually proves the app runs.
type AppVerifyTool struct{}

// AppVerifyInput is the typed input for AppVerifyTool.
type AppVerifyInput struct {
Action string `json:"action"`
Path string `json:"path"`
ReadinessSeconds int `json:"readiness_seconds"`
}

func (AppVerifyTool) Name() string { return "AppVerify" }
func (AppVerifyTool) RiskLevel() string { return "medium" }
func (AppVerifyTool) Aliases() []string { return []string{"app-verify", "verify_app"} }
func (AppVerifyTool) Description() string {
return "Detect how this project boots and prove it runs: infer an install/build/test/start recipe, persist it to .rho/verify/environment.json as the verification contract, and run a bounded boot smoke check with readiness polling. Use action=detect to inspect, action=manifest to write/update the contract, action=smoke to boot the app and verify readiness."
}

func (AppVerifyTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"enum": []string{"detect", "manifest", "smoke"},
"description": "detect infers the recipe; manifest loads-or-detects and persists .rho/verify/environment.json; smoke boots the app using the recipe's start command and polls readiness.",
},
"path": map[string]interface{}{
"type": "string",
"description": "Project directory (default: session working directory).",
},
"readiness_seconds": map[string]interface{}{
"type": "integer",
"minimum": 1,
"maximum": 300,
"description": "Max seconds to wait for the app to become ready during smoke (default 60).",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (AppVerifyTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"action": {Type: "string", Enum: []interface{}{"detect", "manifest", "smoke"}, Description: "detect infers the recipe; manifest loads-or-detects and persists .rho/verify/environment.json; smoke boots the app using the recipe's start command and polls readiness."},
"path": {Type: "string", Description: "Project directory (default: session working directory)."},
"readiness_seconds": {Type: "integer", Minimum: 1, Maximum: 300, Description: "Max seconds to wait for the app to become ready during smoke (default 60)."},
},
"required": []string{"action"},
Required: []string{"action"},
}
}

func (AppVerifyTool) Parameters() map[string]interface{} {
return appVerifySchema.ToJSONSchema()
}

// appVerifySchema is the single source of truth for AppVerify's input schema.
var appVerifySchema = AppVerifyTool{}.Schema()

type smokeResult struct {
Status string `json:"status"` // passed | failed | skipped
StartCmd string `json:"start_command,omitempty"`
Expand All @@ -60,13 +64,9 @@ type smokeResult struct {
}

func (AppVerifyTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var params struct {
Action string `json:"action"`
Path string `json:"path"`
ReadinessSeconds int `json:"readiness_seconds"`
}
if err := json.Unmarshal(input, &params); err != nil {
return "", fmt.Errorf("invalid input: %w", err)
params, err := DecodeInput[AppVerifyInput]("AppVerify", input)
if err != nil {
return "", err
}
switch params.Action {
case "detect", "manifest", "smoke":
Expand Down
51 changes: 27 additions & 24 deletions internal/tool/auto_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,13 @@ type AutoImportTool struct {
importer *AutoImporter
}

// AutoImportInput is the typed input for AutoImportTool.
type AutoImportInput struct {
Code string `json:"code"`
File string `json:"file"`
Apply bool `json:"apply"`
}

// NewAutoImportTool creates an AutoImportTool with a pre-configured AutoImporter.
func NewAutoImportTool() *AutoImportTool {
return &AutoImportTool{
Expand All @@ -894,35 +901,31 @@ func (AutoImportTool) Description() string {
return "Automatically detect and add missing imports in Go source code. Analyzes package-qualified symbol references and adds the corresponding import statements."
}

func (AutoImportTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"code": map[string]interface{}{
"type": "string",
"description": "Go source code to analyze for missing imports",
},
"file": map[string]interface{}{
"type": "string",
"description": "Optional file path for context (used in fix reporting)",
},
"apply": map[string]interface{}{
"type": "boolean",
"description": "If true, return the code with fixes applied; if false, only report missing imports",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (AutoImportTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"code": {Type: "string", Description: "Go source code to analyze for missing imports"},
"file": {Type: "string", Description: "Optional file path for context (used in fix reporting)"},
"apply": {Type: "boolean", Description: "If true, return the code with fixes applied; if false, only report missing imports"},
},
"required": []string{"code"},
Required: []string{"code"},
}
}

func (AutoImportTool) Parameters() map[string]interface{} {
return autoImportSchema.ToJSONSchema()
}

// autoImportSchema is the single source of truth for AutoImport's input schema.
var autoImportSchema = AutoImportTool{}.Schema()

func (t AutoImportTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Code string `json:"code"`
File string `json:"file"`
Apply bool `json:"apply"`
}
if err := json.Unmarshal(input, &p); err != nil {
return "", fmt.Errorf("invalid input: %w", err)
p, err := DecodeInput[AutoImportInput]("AutoImport", input)
if err != nil {
return "", err
}
if p.Code == "" {
return "", fmt.Errorf("code is required")
Expand Down
52 changes: 27 additions & 25 deletions internal/tool/conflict_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,42 +518,44 @@ func describeResolution(c *Conflict) string {
// ConflictResolverTool resolves git merge conflicts in files.
type ConflictResolverTool struct{}

// ConflictResolverInput is the typed input for ConflictResolverTool.
type ConflictResolverInput struct {
Path string `json:"path"`
Strategy string `json:"strategy"`
DryRun bool `json:"dry_run"`
}

func (ConflictResolverTool) Name() string { return "ResolveConflicts" }

func (ConflictResolverTool) Description() string {
return "Automatically resolve git merge conflicts in files. Supports smart resolution strategies including import merging, additive merging, and trivial conflict resolution."
}

func (ConflictResolverTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]interface{}{
"type": "string",
"description": "Absolute path to the file with merge conflicts",
},
"strategy": map[string]interface{}{
"type": "string",
"description": "Resolution strategy: 'smart' (default), 'ours', or 'theirs'",
"enum": []string{"smart", "ours", "theirs"},
},
"dry_run": map[string]interface{}{
"type": "boolean",
"description": "If true, show resolution plan without modifying the file",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (ConflictResolverTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"path": {Type: "string", Description: "Absolute path to the file with merge conflicts"},
"strategy": {Type: "string", Enum: []interface{}{"smart", "ours", "theirs"}, Description: "Resolution strategy: 'smart' (default), 'ours', or 'theirs'"},
"dry_run": {Type: "boolean", Description: "If true, show resolution plan without modifying the file"},
},
"required": []string{"path"},
Required: []string{"path"},
}
}

func (ConflictResolverTool) Parameters() map[string]interface{} {
return conflictResolverSchema.ToJSONSchema()
}

// conflictResolverSchema is the single source of truth for ConflictResolver's input schema.
var conflictResolverSchema = ConflictResolverTool{}.Schema()

func (ConflictResolverTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Path string `json:"path"`
Strategy string `json:"strategy"`
DryRun bool `json:"dry_run"`
}
if err := json.Unmarshal(input, &p); err != nil {
return "", fmt.Errorf("invalid input: %w", err)
p, err := DecodeInput[ConflictResolverInput]("ConflictResolver", input)
if err != nil {
return "", err
}
if p.Path == "" {
return "", fmt.Errorf("path is required")
Expand Down
15 changes: 11 additions & 4 deletions internal/tool/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,13 +353,20 @@ type CronListTool struct{}
func (CronListTool) Name() string { return "CronList" }
func (CronListTool) Aliases() []string { return []string{"cron_list"} }
func (CronListTool) Description() string { return "List all scheduled cron jobs" }

// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (CronListTool) Schema() ToolSchema {
return ToolSchema{Type: "object", Properties: map[string]SchemaProperty{}}
}

func (CronListTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
}
return cronListSchema.ToJSONSchema()
}

// cronListSchema is the single source of truth for CronList's input schema.
var cronListSchema = CronListTool{}.Schema()

func (CronListTool) Execute(_ context.Context, _ json.RawMessage) (string, error) {
jobs := globalCronScheduler.List()
items := make([]map[string]any, 0, len(jobs))
Expand Down
51 changes: 26 additions & 25 deletions internal/tool/extra.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,42 +165,43 @@ func (ConfigTool) Execute(ctx context.Context, input json.RawMessage) (string, e
// Text outside this tool is visible in the detail view; the answer lives here.
type BriefTool struct{}

// BriefInput is the typed input for BriefTool.
type BriefInput struct {
Message string `json:"message"`
Attachments []string `json:"attachments"`
Status string `json:"status"`
}

func (BriefTool) Name() string { return "SendUserMessage" }
func (BriefTool) Aliases() []string { return []string{"brief", "Brief"} }
func (BriefTool) Description() string {
return "Send a message to the user. Supports markdown. Use 'proactive' status when surfacing something the user hasn't asked for."
}

func (BriefTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"message": map[string]interface{}{
"type": "string",
"description": "The message for the user. Supports markdown formatting.",
},
"attachments": map[string]interface{}{
"type": "array",
"items": map[string]interface{}{"type": "string"},
"description": "Optional file paths to attach (images, diffs, logs)",
},
"status": map[string]interface{}{
"type": "string",
"enum": []string{"normal", "proactive"},
"description": "Use 'proactive' when surfacing something the user hasn't asked for",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (BriefTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"message": {Type: "string", Description: "The message for the user. Supports markdown formatting."},
"attachments": {Type: "array", Items: &SchemaProperty{Type: "string"}, Description: "Optional file paths to attach (images, diffs, logs)"},
"status": {Type: "string", Enum: []interface{}{"normal", "proactive"}, Description: "Use 'proactive' when surfacing something the user hasn't asked for"},
},
"required": []string{"message"},
Required: []string{"message"},
}
}

func (BriefTool) Parameters() map[string]interface{} {
return briefSchema.ToJSONSchema()
}

// briefSchema is the single source of truth for Brief's input schema.
var briefSchema = BriefTool{}.Schema()

func (BriefTool) Execute(_ context.Context, input json.RawMessage) (string, error) {
var p struct {
Message string `json:"message"`
Attachments []string `json:"attachments"`
Status string `json:"status"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[BriefInput]("SendUserMessage", input)
if err != nil {
return "", err
}
if p.Message == "" {
Expand Down
Loading
Loading