Skip to content
This repository was archived by the owner on Apr 16, 2026. It is now read-only.

Commit 2d00c95

Browse files
authored
Merge pull request #24 from yepzdk/feat/issue-15-capability-discovery
feat: surface capabilities to users
2 parents 861e88c + 246cfe8 commit 2d00c95

File tree

7 files changed

+269
-7
lines changed

7 files changed

+269
-7
lines changed

internal/cli/info.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strconv"
7+
8+
"github.com/itk-dev/itkdev-claude-code/internal/config"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var infoCmd = &cobra.Command{
13+
Use: "info",
14+
Short: "Show capabilities, hooks, commands, and MCP tools",
15+
RunE: func(cmd *cobra.Command, args []string) error {
16+
out := cmd.OutOrStdout()
17+
info := buildInfo()
18+
19+
if jsonOutput {
20+
return json.NewEncoder(out).Encode(info)
21+
}
22+
23+
fmt.Fprintf(out, "%s v%s\n\n", info.Name, info.Version)
24+
25+
fmt.Fprintln(out, "Features:")
26+
for _, f := range info.Features {
27+
fmt.Fprintf(out, " %-18s %s\n", f.Name, f.Description)
28+
}
29+
30+
fmt.Fprintln(out)
31+
fmt.Fprintln(out, "Commands:")
32+
for _, c := range info.Commands {
33+
fmt.Fprintf(out, " %-18s %s\n", c.Name, c.Description)
34+
}
35+
36+
fmt.Fprintln(out)
37+
fmt.Fprintln(out, "Hooks:")
38+
for _, h := range info.Hooks {
39+
fmt.Fprintf(out, " %-18s %s\n", h.Name, h.Description)
40+
}
41+
42+
fmt.Fprintln(out)
43+
fmt.Fprintln(out, "MCP Tools:")
44+
for _, m := range info.MCPTools {
45+
fmt.Fprintf(out, " %-18s %s\n", m.Name, m.Description)
46+
}
47+
48+
fmt.Fprintln(out)
49+
fmt.Fprintln(out, "Skills:")
50+
for _, s := range info.Skills {
51+
fmt.Fprintf(out, " %-18s %s\n", s.Name, s.Description)
52+
}
53+
54+
fmt.Fprintln(out)
55+
fmt.Fprintf(out, "Console: http://localhost:%s\n", strconv.Itoa(config.DefaultPort))
56+
fmt.Fprintf(out, "Docs: docs/usage.md\n")
57+
58+
return nil
59+
},
60+
}
61+
62+
// InfoData holds all capability information for JSON output.
63+
type InfoData struct {
64+
Name string `json:"name"`
65+
Version string `json:"version"`
66+
Features []InfoEntry `json:"features"`
67+
Commands []InfoEntry `json:"commands"`
68+
Hooks []InfoEntry `json:"hooks"`
69+
MCPTools []InfoEntry `json:"mcp_tools"`
70+
Skills []InfoEntry `json:"skills"`
71+
}
72+
73+
// InfoEntry is a name-description pair used across all info sections.
74+
type InfoEntry struct {
75+
Name string `json:"name"`
76+
Description string `json:"description"`
77+
}
78+
79+
func buildInfo() InfoData {
80+
return InfoData{
81+
Name: config.DisplayName,
82+
Version: config.Version(),
83+
Features: []InfoEntry{
84+
{"Quality Hooks", "Lint, format, and TDD enforcement on every edit"},
85+
{"Context Mgmt", "Context monitor with Endless Mode at 90%"},
86+
{"Memory", "Persistent observations with semantic search"},
87+
{"Workflows", "/spec (plan → implement → verify)"},
88+
{"Web Console", "Observations, sessions, plans, and search"},
89+
{"Worktree", "Git worktree isolation for safe parallel work"},
90+
},
91+
Commands: []InfoEntry{
92+
{"icc run", "Launch Claude Code with hooks and memory"},
93+
{"icc install", "Set up project configuration"},
94+
{"icc serve", "Standalone console server"},
95+
{"icc info", "Show this capabilities reference"},
96+
{"icc greet", "Print the welcome banner"},
97+
{"icc worktree", "Git worktree management"},
98+
{"icc session list", "List sessions"},
99+
{"icc check-context", "Show current context usage"},
100+
{"icc send-clear", "Send clear signal to session"},
101+
},
102+
Hooks: []InfoEntry{
103+
{"file-checker", "Language-aware lint and format on file writes"},
104+
{"tdd-enforcer", "Enforce test-first development order"},
105+
{"branch-guard", "Prevent direct commits to main"},
106+
{"context-monitor", "Track context usage, trigger Endless Mode"},
107+
{"tool-redirect", "Block or redirect tool calls"},
108+
{"spec-stop-guard", "Prevent premature stop during /spec"},
109+
{"spec-plan-validator", "Validate plan file structure"},
110+
{"spec-verify-validator", "Validate verification results"},
111+
{"task-tracker", "Track task creation and updates"},
112+
{"notify", "Desktop notifications on stop"},
113+
},
114+
MCPTools: []InfoEntry{
115+
{"search", "Semantic search across observations"},
116+
{"save_memory", "Save observations to persistent memory"},
117+
{"timeline", "Get chronological observation context"},
118+
{"get_observations", "Retrieve observations by filter"},
119+
},
120+
Skills: []InfoEntry{
121+
{"/spec", "Plan, implement, and verify with TDD"},
122+
{"/spec-plan", "Planning phase only"},
123+
{"/spec-implement", "Implementation phase only"},
124+
{"/spec-verify", "Verification phase only"},
125+
{"/itkdev-issue-workflow", "Autonomous GitHub issue workflow"},
126+
},
127+
}
128+
}
129+
130+
func init() {
131+
rootCmd.AddCommand(infoCmd)
132+
}

internal/cli/info_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package cli
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestInfoCommand(t *testing.T) {
11+
jsonOutput = false // reset global flag from other tests
12+
buf := new(bytes.Buffer)
13+
rootCmd.SetOut(buf)
14+
rootCmd.SetArgs([]string{"info"})
15+
16+
if err := rootCmd.Execute(); err != nil {
17+
t.Fatalf("info command failed: %v", err)
18+
}
19+
20+
output := buf.String()
21+
22+
sections := []string{"Features:", "Commands:", "Hooks:", "MCP Tools:", "Skills:"}
23+
for _, section := range sections {
24+
if !strings.Contains(output, section) {
25+
t.Errorf("output missing section %q", section)
26+
}
27+
}
28+
29+
entries := []string{"file-checker", "icc run", "search", "/spec"}
30+
for _, entry := range entries {
31+
if !strings.Contains(output, entry) {
32+
t.Errorf("output missing entry %q", entry)
33+
}
34+
}
35+
}
36+
37+
func TestInfoCommandJSON(t *testing.T) {
38+
buf := new(bytes.Buffer)
39+
rootCmd.SetOut(buf)
40+
rootCmd.SetArgs([]string{"info", "--json"})
41+
42+
if err := rootCmd.Execute(); err != nil {
43+
t.Fatalf("info --json command failed: %v", err)
44+
}
45+
46+
var data InfoData
47+
if err := json.Unmarshal(buf.Bytes(), &data); err != nil {
48+
t.Fatalf("invalid JSON output: %v", err)
49+
}
50+
51+
if data.Name == "" {
52+
t.Error("JSON name field is empty")
53+
}
54+
if data.Version == "" {
55+
t.Error("JSON version field is empty")
56+
}
57+
if len(data.Features) == 0 {
58+
t.Error("JSON features list is empty")
59+
}
60+
if len(data.Hooks) == 0 {
61+
t.Error("JSON hooks list is empty")
62+
}
63+
if len(data.MCPTools) == 0 {
64+
t.Error("JSON mcp_tools list is empty")
65+
}
66+
if len(data.Skills) == 0 {
67+
t.Error("JSON skills list is empty")
68+
}
69+
}

internal/cli/run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ func updateSettingsAnnouncement(path string, port int, logger *slog.Logger) {
187187
}
188188

189189
settings["companyAnnouncements"] = []string{
190-
"Console: http://localhost:" + strconv.Itoa(port) + " | /spec — plan, build & verify",
190+
"Console: http://localhost:" + strconv.Itoa(port) + " | /spec — plan, build & verify | Hooks: lint, TDD, branch-guard | Memory: save_memory, search | icc info — full reference",
191191
}
192192

193193
out, err := json.MarshalIndent(settings, "", " ")

internal/cli/run_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestUpdateSettingsAnnouncement(t *testing.T) {
4242
t.Fatalf("expected 1 announcement, got %v", result["companyAnnouncements"])
4343
}
4444

45-
want := "Console: http://localhost:42000 | /spec — plan, build & verify"
45+
want := "Console: http://localhost:42000 | /spec — plan, build & verify | Hooks: lint, TDD, branch-guard | Memory: save_memory, search | icc info — full reference"
4646
if announcements[0] != want {
4747
t.Errorf("announcement = %q, want %q", announcements[0], want)
4848
}

internal/console/context/builder.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ func (b *Builder) Build(observations []*db.Observation, summaries []*db.Summary)
7575
}
7676
}
7777

78+
// Always append a capabilities summary so Claude knows what's available
79+
capabilities := `## Active Features
80+
- **Quality Hooks**: file-checker (lint/format), tdd-enforcer, branch-guard (no commits to main)
81+
- **Context Management**: context-monitor with Endless Mode at 90%
82+
- **Persistent Memory**: save_memory() to record discoveries, search() to find them
83+
- **Workflows**: /spec (plan → implement → verify), /itkdev-issue-workflow
84+
- **Web Console**: observations, sessions, plans, search at the console URL
85+
- **Worktree Isolation**: icc worktree for safe parallel work`
86+
87+
capTokens := EstimateTokens(capabilities)
88+
if usedTokens+capTokens <= b.maxTokens {
89+
parts = append(parts, capabilities)
90+
}
91+
7892
if len(parts) == 0 {
7993
return ""
8094
}

internal/console/context/builder_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,41 @@ func TestBuildTruncatesToTokenBudget(t *testing.T) {
8787
}
8888
}
8989

90+
func TestBuildIncludesCapabilities(t *testing.T) {
91+
b := NewBuilder(4000)
92+
93+
// Even with no observations or summaries, capabilities should not appear
94+
// (the builder returns empty when both inputs are nil)
95+
result := b.Build(nil, nil)
96+
if result != "" {
97+
t.Errorf("expected empty string for nil inputs, got %q", result)
98+
}
99+
100+
// With observations, capabilities section should be appended
101+
obs := []*db.Observation{
102+
{ID: 1, Type: "discovery", Title: "Test", Text: "Test observation"},
103+
}
104+
result = b.Build(obs, nil)
105+
106+
if !containsAll(result, "Active Features", "Quality Hooks", "Persistent Memory", "/spec") {
107+
t.Errorf("result missing capabilities section: %s", result)
108+
}
109+
}
110+
111+
func TestBuildCapabilitiesOmittedWhenBudgetTight(t *testing.T) {
112+
b := NewBuilder(50) // very tight budget
113+
114+
obs := []*db.Observation{
115+
{ID: 1, Type: "discovery", Title: "Important", Text: "Critical finding that takes up budget"},
116+
}
117+
result := b.Build(obs, nil)
118+
119+
// Capabilities should be omitted when budget is too tight
120+
if containsAll(result, "Active Features") {
121+
t.Error("capabilities should be omitted when token budget is tight")
122+
}
123+
}
124+
90125
func TestEstimateTokens(t *testing.T) {
91126
tests := []struct {
92127
input string

internal/installer/steps/config_files.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,24 @@ func settingsJSON(binPath string) []byte {
125125
"enableAllProjectMcpServers": true,
126126
"respectGitignore": false,
127127
"alwaysThinkingEnabled": true,
128-
"spinnerTipsEnabled": false,
129-
"prefersReducedMotion": true,
130-
"showTurnDuration": false,
131-
"cleanupPeriodDays": 7,
128+
"spinnerTipsEnabled": true,
129+
"spinnerTips": []string{
130+
"Use save_memory() to record important discoveries for future sessions",
131+
"/spec guides you through plan → implement → verify with TDD",
132+
"The web console tracks all observations, sessions, and plans",
133+
"Context monitor triggers Endless Mode at 90% — no manual intervention needed",
134+
"Branch guard prevents accidental commits to main",
135+
"File checker runs lint and format automatically on every file write",
136+
"Use icc info to see all available features, hooks, and tools",
137+
"Worktree isolation lets you work on multiple branches safely in parallel",
138+
"Search across all observations with the search MCP tool",
139+
"TDD enforcer ensures tests are written before implementation",
140+
},
141+
"prefersReducedMotion": true,
142+
"showTurnDuration": false,
143+
"cleanupPeriodDays": 7,
132144
"companyAnnouncements": []string{
133-
"Console: http://localhost:" + strconv.Itoa(config.DefaultPort) + " | /spec — plan, build & verify",
145+
"Console: http://localhost:" + strconv.Itoa(config.DefaultPort) + " | /spec — plan, build & verify | Hooks: lint, TDD, branch-guard | Memory: save_memory, search | icc info — full reference",
134146
},
135147
}
136148
data, _ := json.MarshalIndent(settings, "", " ")

0 commit comments

Comments
 (0)