Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4a57df7
fix(task): release runFunc when a task reaches a terminal state
moshloop Jun 30, 2026
e9dda7e
feat: add oxlint custom rules linting for clicky-ui webapp
moshloop Jul 5, 2026
8e4b3bc
feat: add support for hidden flags in CLI and schema generation
moshloop Jul 5, 2026
93acde3
feat: update model catalog to latest provider tiers
moshloop Jul 5, 2026
3b77166
feat Add HTTP request timing middleware and context utilities
moshloop Jul 5, 2026
0b8271b
fix(rpc): Fix route deduplication for wildcard parameter name differe…
moshloop Jul 5, 2026
b376f0a
feat: preserve schema field order in markdown formatter output
moshloop Jul 7, 2026
301ba8e
fix: Replace fmt.Printf with clicky logger and fix timing middleware …
moshloop Jul 7, 2026
8b085cd
chore: add UI and linting dependencies to webapp
moshloop Jul 7, 2026
956ee0a
chore: update Go module dependencies to latest versions
moshloop Jul 7, 2026
cf2e9ec
claude: task output review phase 1 - fix log display off-by-one, capt…
moshloop Jul 9, 2026
dfc0aa3
feat(entity): add MCPToolHints for tool metadata and annotations
moshloop Jul 9, 2026
4dc6c6d
feat(aichat): Add default permission and tool metadata support with c…
moshloop Jul 9, 2026
27d0c52
feat(mcp): add MCP tool annotations and clicky meta support
moshloop Jul 9, 2026
be582a5
fix(task): fix data races and render lifecycle for task output
moshloop Jul 9, 2026
939ebf0
claude: stamp default tool permissions on standard entity verbs
moshloop Jul 12, 2026
b455a7a
refactor(aichat): centralize model and tool metadata
moshloop Jul 12, 2026
e9d971c
feat(rpc): expose swagger command executor
moshloop Jul 12, 2026
4de28a4
docs(skills): add clicky development guides
moshloop Jul 12, 2026
d4bf5b9
docs(docs): delegate agent workflow guidance to shared instructions
moshloop Jul 15, 2026
bdf1894
feat(api): support semantic columns and structured table values
moshloop Jul 15, 2026
d02478d
feat(api): Add streaming table exports and export metadata
moshloop Jul 15, 2026
021d1b6
build(db): Update commons-db dependency to v0.1.14
moshloop Jul 15, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ clicky
!task/ui/tsconfig.json
!examples/enitity/Taskfile.yaml
!examples/enitity/webapp/index.html
!examples/enitity/webapp/.oxlintrc.json
!examples/enitity/webapp/package.json
!examples/enitity/webapp/pnpm-lock.yaml
!skills/*/agents/openai.yaml
!examples/enitity/webapp/pnpm-workspace.yaml
!examples/enitity/webapp/tsconfig.json
!examples/enitity/webapp/dist/
Expand Down
125 changes: 7 additions & 118 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,122 +1,11 @@
## Grite
# clicky — agent notes

This repository uses **Grite** as the canonical task and memory system. **Always use grite commands** (not git) for task/issue tracking.
Shared ways of working, the gavel todo workflow, and global skills come from the root ~/.agents/AGENTS.md.

### When to use grite
## Memory

- **"What tasks are done/open?"** → `grite issue list`
- **"What did I work on?"** → `grite issue list --state closed`
- **"What do I know about X?"** → `grite issue list --label memory`
- **"What should I work on next?"** → `grite issue dep topo --state open`
- **"What does function X do?"** → `grite context query X`
- **Starting a new task** → `grite issue create`
- **Making progress** → `grite issue comment`
- **Task A depends on B** → `grite issue dep add`
See the [project memory index](.agents/memory/index.md).

### Startup routine

Run at the beginning of each session:

```bash
grite sync --pull --json
grite issue list --json
grite issue dep topo --state open --json
```

### Creating tasks/memories

```bash
# Create a task
grite issue create --title "Task title" --body "Description" --label agent:todo --json

# Store a discovery as memory
grite issue create --title "[Memory] Topic" --body "What you learned..." --label memory --json
```

### Working on issues

```bash
# Add a comment with your plan before coding
grite issue comment <ID> --body "Plan: ..." --json

# Add checkpoint comments after milestones
grite issue comment <ID> --body "Checkpoint: what changed, why, tests run" --json

# Close when done
grite issue close <ID> --json
grite sync --push --json
```

### Querying tasks

```bash
# All issues
grite issue list --json

# Open tasks
grite issue list --state open --json

# Completed tasks
grite issue list --state closed --json

# Memories
grite issue list --label memory --json
```

### Dependencies

Track task ordering and blockers with a dependency DAG:

```bash
# Task A depends on Task B (B must complete first)
grite issue dep add <A> --target <B> --type depends_on --json

# Task A blocks Task B (A must complete first)
grite issue dep add <A> --target <B> --type blocks --json

# Mark tasks as related (no ordering constraint)
grite issue dep add <A> --target <B> --type related_to --json

# List what an issue depends on
grite issue dep list <ID> --json

# List what depends on an issue (reverse)
grite issue dep list <ID> --reverse --json

# Get execution order (topological sort of open tasks)
grite issue dep topo --state open --json
```

**Always run `dep topo`** at session start to determine which task to work on next.

### Context store

Index and query codebase structure for fast navigation:

```bash
# Index all tracked files (skips unchanged files)
grite context index --json

# Index specific files or patterns
grite context index --path src/main.py --json
grite context index --pattern "*.rs" --json

# Query for a symbol (function, class, struct, etc.)
grite context query <symbol_name> --json

# Show all symbols in a file
grite context show <file_path> --json

# Set project-level context (conventions, architecture notes)
grite context set <key> <value> --json

# View all project context
grite context project --json
```

**Index after significant code changes** to keep the symbol database current.

### Key flags

- `--json` - Use for all commands (machine-readable output)
- `--quiet` - Suppress human output
## Skills
- [clicky-tasks](skills/clicky-tasks/SKILL.md) — refactor goroutines/WaitGroups to Clicky task APIs (StartTask, StartGroup, typed results, retries, cancellation, task-safe logging).
- [pretty-printing](skills/pretty-printing/SKILL.md) — implement/review Clicky render interfaces and builders (Pretty, PrettyShort, TreeNode, TableProvider, icons, code blocks) instead of rendering ANSI/HTML/Markdown in domain code.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ library in your own application, see [CLAUDE.md](CLAUDE.md) and [README.md](READ
make build # build ./clicky from ./cmd/clicky (NEVER run `go build` directly)
make test # go test -v ./... (ROOT module only — example modules are separate)
make lint # golangci-lint v2.8.0 (pinned into .bin/) + go vet ./...
make lint-clicky-ui # run clicky-ui's custom oxlint rules against the entity demo webapp
make fmt # gofmt -s -w . and `go mod tidy` across every module in GO_MODULES
make check # fmt + lint + test
make task-ui # rebuild the embedded Preact task-UI bundle (task/ui/dist/taskui.js)
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,14 @@ install: build
mv clicky /usr/local/bin/clicky

# Run linter
.PHONY: lint
.PHONY: lint lint-clicky-ui
lint: golangci-lint
$(GOLANGCI_LINT) run ./...
go vet ./...

lint-clicky-ui:
cd examples/enitity/webapp && pnpm install --frozen-lockfile && pnpm run lint:clicky-ui

.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
Expand Down
27 changes: 3 additions & 24 deletions aichat/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import (
"context"
"fmt"
"net/http"
"os/exec"
"strings"
"time"

_ "github.com/flanksource/captain/pkg/ai/provider" // registers the agent backends
capapi "github.com/flanksource/captain/pkg/api"
"github.com/flanksource/commons-db/shell"
)

// AgentOptions configures the captain agent engine (EngineAgent models). The
Expand Down Expand Up @@ -58,26 +58,6 @@ func defaultAgentProviderFactory(cfg capapi.Config) (capapi.StreamingProvider, e
return sp, nil
}

// agentModelConfigured reports whether the local backend for an agent model is
// installed (best effort): codex needs the `codex` binary; claude-agent needs
// `tsx` on PATH (or a provisioned agent dir, created lazily). A turn still fails
// loud if the probe is wrong.
func agentModelConfigured(m Model) bool {
switch m.Backend {
case capapi.BackendCodexCLI:
return lookPath("codex")
case capapi.BackendClaudeAgent, capapi.BackendClaudeCLI:
return lookPath("tsx")
default:
return false
}
}

func lookPath(bin string) bool {
_, err := exec.LookPath(bin)
return err == nil
}

// captainModel returns the model slug passed to the captain backend: AgentModel
// when set, otherwise the menu ID.
func captainModel(m Model) string {
Expand Down Expand Up @@ -143,11 +123,10 @@ func (s *Server) agentRequest(req ChatRequest, resumeID string) capapi.Spec {
return capapi.Spec{
Prompt: capapi.Prompt{User: prompt, System: s.system},
Model: capapi.Model{Effort: capapi.Effort(req.ReasoningEffort), Temperature: req.Temperature},
Budget: capapi.Budget{Cost: req.Budget.Cost, MaxTokens: req.Budget.MaxTokens},
Budget: capapi.Budget{Cost: req.Budget.Cost, MaxTokens: req.Budget.MaxTokens, MaxTurns: s.opts.Agent.MaxTurns},
Permissions: perms,
Context: capapi.Context{Dir: s.opts.Agent.Cwd},
Setup: &shell.Setup{Cwd: s.opts.Agent.Cwd},
SessionID: resumeID,
MaxTurns: s.opts.Agent.MaxTurns,
}
}

Expand Down
48 changes: 27 additions & 21 deletions aichat/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ func containsType(parts []map[string]any, typ string) bool {
// agentServer builds a server whose agent engine uses the supplied fake,
// recording every Config the factory is asked to build.
func agentServer(opts Options, fake *fakeStreamProvider, configs *[]capapi.Config) *Server {
// Register a stable test agent model so LookupModel routes these requests to
// the agent path regardless of the concrete ids captain's catalog ships. The
// upsert is idempotent and process-global; a fake provider factory serves the
// turn, so no real backend is contacted.
_ = RegisterModel(Model{ID: "claude-agent-sonnet", Backend: capapi.BackendClaudeAgent, Label: "Claude Agent · Sonnet (test)", Reasoning: true, ContextWindow: 200000})
opts.AgentProviderFactory = func(cfg capapi.Config) (capapi.StreamingProvider, error) {
if configs != nil {
*configs = append(*configs, cfg)
Expand Down Expand Up @@ -367,8 +372,8 @@ func TestAgentRequestEditOptInSkipsReadOnlyDefault(t *testing.T) {
if !air.Permissions.HasPreset(capapi.PresetEdit) {
t.Errorf("Edit should be true")
}
if air.Context.Dir != "/repo" {
t.Errorf("Cwd = %q, want /repo", air.Context.Dir)
if air.Cwd() != "/repo" {
t.Errorf("Cwd = %q, want /repo", air.Cwd())
}
if len(air.Permissions.Tools.Allow) != 0 {
t.Errorf("AllowedTools = %v, want empty (edit opt-in lets the backend curate)", air.Permissions.Tools.Allow)
Expand All @@ -393,29 +398,30 @@ func TestAgentRequestCarriesBudgetAndTemperature(t *testing.T) {
}
}

// TestAgentModelsInCatalog verifies the captain-owned catalog (consumed via the
// aichat aliases) carries both agent and Genkit models, that IsAgent() tracks the
// backend kind, and that every id round-trips through LookupModel with the slug
// the captain backend receives.
func TestAgentModelsInCatalog(t *testing.T) {
cases := map[string]struct {
engine Engine
backend capapi.Backend
agentModel string
}{
"claude-agent-sonnet": {EngineAgent, capapi.BackendClaudeAgent, ""},
"codex-gpt-5-codex": {EngineAgent, capapi.BackendCodexCLI, "gpt-5-codex"},
"anthropic/claude-sonnet-4-6": {EngineGenkit, "", ""},
}
for id, want := range cases {
m, err := LookupModel(id)
var sawAgent, sawGenkit bool
for _, model := range Catalog() {
got, err := LookupModel(model.ID)
if err != nil {
t.Fatalf("LookupModel(%q): %v", id, err)
t.Fatalf("LookupModel(%q): %v", model.ID, err)
}
if m.Engine != want.engine {
t.Errorf("%s engine = %q, want %q", id, m.Engine, want.engine)
if got.IsAgent() != (model.Backend.Kind() == "cli") {
t.Errorf("%s IsAgent = %v, but backend %q kind = %q", model.ID, got.IsAgent(), model.Backend, model.Backend.Kind())
}
if m.Backend != want.backend {
t.Errorf("%s backend = %q, want %q", id, m.Backend, want.backend)
}
if captainModel(m) != firstNonEmptyString(want.agentModel, id) {
t.Errorf("%s captainModel = %q, want %q", id, captainModel(m), firstNonEmptyString(want.agentModel, id))
if model.IsAgent() {
sawAgent = true
if want := firstNonEmptyString(model.AgentModel, model.ID); captainModel(model) != want {
t.Errorf("%s captainModel = %q, want %q", model.ID, captainModel(model), want)
}
} else {
sawGenkit = true
}
}
if !sawAgent || !sawGenkit {
t.Fatalf("catalog should contain both agent and genkit models: agent=%v genkit=%v", sawAgent, sawGenkit)
}
}
60 changes: 49 additions & 11 deletions aichat/approval.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"

"github.com/firebase/genkit/go/ai"
clickymcp "github.com/flanksource/clicky/mcp"
"github.com/flanksource/clicky/rpc"
)

Expand All @@ -15,24 +16,31 @@ const approvalKey = "approval"
type ToolMode string

const (
ToolModeEnabled ToolMode = "enabled"
ToolModeAsk ToolMode = "ask"
ToolModeDisabled ToolMode = "disabled"
ToolModeOn ToolMode = "on"
ToolModeAsk ToolMode = "ask"
ToolModeOff ToolMode = "off"
ToolModeAuto ToolMode = "auto"

// Backward-compatible aliases for the labels older clients send.
ToolModeEnabled ToolMode = ToolModeOn
ToolModeDisabled ToolMode = ToolModeOff
)

// ToolPreferences carries the clicky-ui tool preference payload. The UI sends
// "enabled", "ask", or "disabled"; "auto" and "off" are accepted for older
// callers that used those labels.
// "on", "ask", "off", or "auto"; "enabled" and "disabled" are accepted for
// older callers that used those labels.
type ToolPreferences map[string]ToolMode

func normalizeToolMode(mode ToolMode) (ToolMode, bool) {
switch ToolMode(strings.ToLower(strings.TrimSpace(string(mode)))) {
case ToolModeEnabled, "auto":
return ToolModeEnabled, true
case ToolModeOn, "enabled":
return ToolModeOn, true
case ToolModeAsk:
return ToolModeAsk, true
case ToolModeDisabled, "off":
return ToolModeDisabled, true
case ToolModeOff, "disabled":
return ToolModeOff, true
case ToolModeAuto:
return ToolModeAuto, true
default:
return "", false
}
Expand All @@ -49,8 +57,15 @@ type ToolInfo struct {
// Group is the tool-group this tool belongs to (clicky/tool-group). When
// non-empty the preferences UI presents the group as a single entry and the
// group's preference governs every member tool.
Group string
Operation *rpc.RPCOperation
Group string
Parent string
Icon string
DefaultPermission ToolMode
Strict *bool
ReadOnlyHint *bool
DestructiveHint *bool
IdempotentHint *bool
Operation *rpc.RPCOperation
}

func toolInfo(name string, op *rpc.RPCOperation) ToolInfo {
Expand All @@ -62,9 +77,32 @@ func toolInfo(name string, op *rpc.RPCOperation) ToolInfo {
info.Method = strings.ToUpper(op.Method)
info.Path = op.Path
info.Group = op.Group
info.DefaultPermission = ToolMode(op.ToolHints.DefaultPermission)
info.Parent = op.ToolHints.Parent
info.Icon = op.ToolHints.Icon
info.Strict = op.ToolHints.Strict
hints := clickymcp.EffectiveToolHints(op)
info.ReadOnlyHint = hints.ReadOnlyHint
info.DestructiveHint = hints.DestructiveHint
info.IdempotentHint = hints.IdempotentHint
if op.Clicky != nil {
info.ClickyVerb = op.Clicky.Verb
info.ClickyScope = op.Clicky.Scope
if info.Group == "" {
info.Group = op.Clicky.Group
}
if info.DefaultPermission == "" {
info.DefaultPermission = ToolMode(op.Clicky.ToolHints.DefaultPermission)
}
if info.Parent == "" {
info.Parent = op.Clicky.ToolHints.Parent
}
if info.Icon == "" {
info.Icon = op.Clicky.ToolHints.Icon
}
if info.Strict == nil {
info.Strict = op.Clicky.ToolHints.Strict
}
}
return info
}
Expand Down
Loading
Loading