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
439 changes: 439 additions & 0 deletions internal/tool/schema_batch_test.go

Large diffs are not rendered by default.

407 changes: 270 additions & 137 deletions internal/tool/spec.go

Large diffs are not rendered by default.

65 changes: 32 additions & 33 deletions internal/tool/spec_adaptive.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ import (
type SpecAdaptiveTool struct{}

func (SpecAdaptiveTool) Name() string { return "SpecAdaptive" }

// SpecAdaptiveInput is the typed input for SpecAdaptiveTool.
type SpecAdaptiveInput struct {
TaskID string `json:"task_id"`
EstimatedEffort int `json:"estimated_effort"`
ActualEffort int `json:"actual_effort"`
UnplannedDeps int `json:"unplanned_deps"`
SuperScore float64 `json:"super_score"`
}

func (SpecAdaptiveTool) Aliases() []string {
return []string{"spec_adaptive", "spec:adaptive"}
}
Expand All @@ -22,50 +32,39 @@ func (SpecAdaptiveTool) Description() string {
return "Collect execution telemetry and compute drift score. Compares actual effort vs estimated, S.U.P.E.R compliance, and unplanned dependencies. Returns drift level (none/mild/significant/severe) and recommended corrective action."
}

func (SpecAdaptiveTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"task_id": map[string]interface{}{
"type": "string",
"description": "Task identifier that was just completed",
},
"estimated_effort": map[string]interface{}{
"type": "integer",
"description": "Estimated effort in minutes",
},
"actual_effort": map[string]interface{}{
"type": "integer",
"description": "Actual effort in minutes",
},
"unplanned_deps": map[string]interface{}{
"type": "integer",
"description": "Number of unplanned dependencies encountered",
},
"super_score": map[string]interface{}{
"type": "number",
"description": "S.U.P.E.R compliance score 0.0-1.0",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (SpecAdaptiveTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"task_id": {Type: "string", Description: "Task identifier that was just completed"},
"estimated_effort": {Type: "integer", Description: "Estimated effort in minutes"},
"actual_effort": {Type: "integer", Description: "Actual effort in minutes"},
"unplanned_deps": {Type: "integer", Description: "Number of unplanned dependencies encountered"},
"super_score": {Type: "number", Description: "S.U.P.E.R compliance score 0.0-1.0"},
},
"required": []string{"task_id"},

Required: []string{"task_id"},
}
}

func (SpecAdaptiveTool) Parameters() map[string]interface{} {
return specAdaptiveSchema.ToJSONSchema()
}

// specAdaptiveSchema is the single source of truth for SpecAdaptive's input schema.
var specAdaptiveSchema = SpecAdaptiveTool{}.Schema()

type AdaptiveResult struct {
DriftScore float64 `json:"drift_score"`
DriftLevel string `json:"drift_level"`
Recommendation string `json:"recommendation"`
}

func (SpecAdaptiveTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
TaskID string `json:"task_id"`
EstimatedEffort int `json:"estimated_effort"`
ActualEffort int `json:"actual_effort"`
UnplannedDeps int `json:"unplanned_deps"`
SuperScore float64 `json:"super_score"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[SpecAdaptiveInput]("SpecAdaptive", input)
if err != nil {
return "", err
}

Expand Down
49 changes: 26 additions & 23 deletions internal/tool/spec_adr.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ import (
type SpecAdrTool struct{}

func (SpecAdrTool) Name() string { return "SpecAdr" }

// SpecAdrInput is the typed input for SpecAdrTool.
type SpecAdrInput struct {
Action string `json:"action"`
Title string `json:"title"`
ReqID string `json:"req_id"`
}

func (SpecAdrTool) Aliases() []string {
return []string{"spec_adr", "spec:adr"}
}
Expand All @@ -20,34 +28,29 @@ func (SpecAdrTool) Description() string {
return "Create and manage Architecture Decision Records. Documents key technical decisions with context, options considered, rationale, and consequences. Links decisions to requirements they satisfy."
}

func (SpecAdrTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"description": "Action: create (new ADR), list (show all), link (link to requirement)",
"enum": []string{"create", "list", "link"},
},
"title": map[string]interface{}{
"type": "string",
"description": "ADR title (required for create)",
},
"req_id": map[string]interface{}{
"type": "string",
"description": "REQ ID to link to (required for link)",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (SpecAdrTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"action": {Type: "string", Enum: []interface{}{"create", "list", "link"}, Description: "Action: create (new ADR), list (show all), link (link to requirement)"},
"title": {Type: "string", Description: "ADR title (required for create)"},
"req_id": {Type: "string", Description: "REQ ID to link to (required for link)"},
},
}
}

func (SpecAdrTool) Parameters() map[string]interface{} {
return specAdrSchema.ToJSONSchema()
}

// specAdrSchema is the single source of truth for SpecAdr's input schema.
var specAdrSchema = SpecAdrTool{}.Schema()

func (SpecAdrTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Action string `json:"action"`
Title string `json:"title"`
ReqID string `json:"req_id"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[SpecAdrInput]("SpecAdr", input)
if err != nil {
return "", err
}
if p.Action == "" {
Expand Down
16 changes: 12 additions & 4 deletions internal/tool/spec_analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,21 @@ func (AnalyzeTool) Description() string {
return "Analyze the active spec for cross-artifact consistency: check that spec requirements are covered by plan and tasks, identify orphaned work, and report quality issues. Read-only analysis — does not modify any files."
}

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

func (AnalyzeTool) Parameters() map[string]interface{} {
return analyzeSchema.ToJSONSchema()
}

// analyzeSchema is the single source of truth for Analyze's input schema.
var analyzeSchema = AnalyzeTool{}.Schema()

func (AnalyzeTool) Execute(ctx context.Context, _ json.RawMessage) (string, error) {
dir, err := specDir(ctx)
if err != nil {
Expand Down
43 changes: 24 additions & 19 deletions internal/tool/spec_bdd.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import (
type SpecBddTool struct{}

func (SpecBddTool) Name() string { return "SpecBdd" }

// SpecBddInput is the typed input for SpecBddTool.
type SpecBddInput struct {
Action string `json:"action"`
Format string `json:"format"`
}

func (SpecBddTool) Aliases() []string {
return []string{"spec_bdd", "spec:bdd"}
}
Expand All @@ -22,24 +29,25 @@ func (SpecBddTool) Description() string {
return "Generate Gherkin/BDD scenarios from requirements. Converts EARS-format requirements into Given/When/Then scenarios for behavior-driven testing. Links scenarios back to requirements for traceability."
}

func (SpecBddTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"action": map[string]interface{}{
"type": "string",
"description": "Action: generate (from spec), validate (check coverage), export (to feature files)",
"enum": []string{"generate", "validate", "export"},
},
"format": map[string]interface{}{
"type": "string",
"description": "Output format: gherkin (default), cucumber, pytest-bdd",
"enum": []string{"gherkin", "cucumber", "pytest-bdd"},
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (SpecBddTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"action": {Type: "string", Enum: []interface{}{"generate", "validate", "export"}, Description: "Action: generate (from spec), validate (check coverage), export (to feature files)"},
"format": {Type: "string", Enum: []interface{}{"gherkin", "cucumber", "pytest-bdd"}, Description: "Output format: gherkin (default), cucumber, pytest-bdd"},
},
}
}

func (SpecBddTool) Parameters() map[string]interface{} {
return specBddSchema.ToJSONSchema()
}

// specBddSchema is the single source of truth for SpecBdd's input schema.
var specBddSchema = SpecBddTool{}.Schema()

type BddScenario struct {
Feature string `json:"feature"`
Scenario string `json:"scenario"`
Expand All @@ -48,11 +56,8 @@ type BddScenario struct {
}

func (SpecBddTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
Action string `json:"action"`
Format string `json:"format"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[SpecBddInput]("SpecBdd", input)
if err != nil {
return "", err
}
if p.Action == "" {
Expand Down
34 changes: 22 additions & 12 deletions internal/tool/spec_blast.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import (
type SpecBlastTool struct{}

func (SpecBlastTool) Name() string { return "SpecBlast" }

// SpecBlastInput is the typed input for SpecBlastTool.
type SpecBlastInput struct {
TargetFile string `json:"target_file"`
}

func (SpecBlastTool) Aliases() []string {
return []string{"spec_blast", "spec:blast"}
}
Expand All @@ -23,18 +29,24 @@ func (SpecBlastTool) Description() string {
return "Blast radius analysis for proposed changes. Estimates which files, functions, and dependencies will be affected by a change before implementation."
}

func (SpecBlastTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"target_file": map[string]interface{}{
"type": "string",
"description": "File to analyze for blast radius",
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (SpecBlastTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"target_file": {Type: "string", Description: "File to analyze for blast radius"},
},
}
}

func (SpecBlastTool) Parameters() map[string]interface{} {
return specBlastSchema.ToJSONSchema()
}

// specBlastSchema is the single source of truth for SpecBlast's input schema.
var specBlastSchema = SpecBlastTool{}.Schema()

type BlastResult struct {
TargetFile string `json:"target_file"`
DirectImpact []string `json:"direct_impact"`
Expand All @@ -45,10 +57,8 @@ type BlastResult struct {
}

func (SpecBlastTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
TargetFile string `json:"target_file"`
}
if err := json.Unmarshal(input, &p); err != nil {
p, err := DecodeInput[SpecBlastInput]("SpecBlast", input)
if err != nil {
return "", err
}
if p.TargetFile == "" {
Expand Down
48 changes: 29 additions & 19 deletions internal/tool/spec_checklist.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import (
type ChecklistTool struct{}

func (ChecklistTool) Name() string { return "Checklist" }

// ChecklistInput is the typed input for ChecklistTool.
type ChecklistInput struct {
IncludeReferences bool `json:"include_references"`
Artifact string `json:"artifact"`
}

func (ChecklistTool) Aliases() []string {
return []string{"checklist", "spec_checklist", "spec:checklist"}
}
Expand All @@ -24,30 +31,33 @@ func (ChecklistTool) Description() string {
return "Generate a QA checklist from the active spec's requirements and scenarios. Each requirement becomes a checkable item. Optionally include reference checklists for accessibility, security, performance, observability, and testing."
}

func (ChecklistTool) Parameters() map[string]interface{} {
return map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"include_references": map[string]interface{}{
"type": "boolean",
"description": "If true, append reference checklists (accessibility, security, performance, observability, testing) alongside spec-derived checks",
},
"artifact": map[string]interface{}{
"type": "string",
"description": "Which artifact to generate checklist from: spec.md (default) or tasks.md",
"enum": []string{"spec.md", "tasks.md"},
},
// Schema returns the typed input schema. Parameters() delegates to it so the
// two cannot diverge.
func (ChecklistTool) Schema() ToolSchema {
return ToolSchema{
Type: "object",
Properties: map[string]SchemaProperty{
"include_references": {Type: "boolean", Description: "If true, append reference checklists (accessibility, security, performance, observability, testing) alongside spec-derived checks"},
"artifact": {Type: "string", Enum: []interface{}{"spec.md", "tasks.md"}, Description: "Which artifact to generate checklist from: spec.md (default) or tasks.md"},
},
}
}

func (ChecklistTool) Parameters() map[string]interface{} {
return checklistSchema.ToJSONSchema()
}

// checklistSchema is the single source of truth for Checklist's input schema.
var checklistSchema = ChecklistTool{}.Schema()

func (ChecklistTool) Execute(ctx context.Context, input json.RawMessage) (string, error) {
var p struct {
IncludeReferences bool `json:"include_references"`
Artifact string `json:"artifact"`
}
if input != nil {
_ = json.Unmarshal(input, &p)
var p ChecklistInput
if len(input) > 0 && string(input) != "null" {
decoded, err := DecodeInput[ChecklistInput]("Checklist", input)
if err != nil {
return "", err
}
p = decoded
}
if p.Artifact == "" {
p.Artifact = "spec.md"
Expand Down
Loading
Loading