Skip to content

Commit b18751a

Browse files
committed
Merge remote-tracking branch 'origin/main' into chore/compat-matrix-0-0-1
2 parents 36b9dee + 168e311 commit b18751a

120 files changed

Lines changed: 4786 additions & 757 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ This project is indexed by GitNexus as **graycode** (97743 symbols, 322940 relat
228228
| `gitnexus://repo/graycode/context` | Codebase overview, check index freshness |
229229
| `gitnexus://repo/graycode/clusters` | All functional areas |
230230
| `gitnexus://repo/graycode/processes` | All execution flows |
231-
| `gitnexus://repo/graycode/process/{name}` | Step-by-step execution swift |
231+
| `gitnexus://repo/graycode/process/{name}` | Step-by-step execution trace |
232232

233233
## CLI
234234

‎CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ This project is indexed by GitNexus as **graycode** (97743 symbols, 322940 relat
2828
| `gitnexus://repo/graycode/context` | Codebase overview, check index freshness |
2929
| `gitnexus://repo/graycode/clusters` | All functional areas |
3030
| `gitnexus://repo/graycode/processes` | All execution flows |
31-
| `gitnexus://repo/graycode/process/{name}` | Step-by-step execution swift |
31+
| `gitnexus://repo/graycode/process/{name}` | Step-by-step execution trace |
3232

3333
## CLI
3434

‎README.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ graycode cloud graph sync <session-id>
128128
graycode cloud graph sync --mission-dir /path/to/mission
129129
```
130130

131+
Cloud commands require an endpoint. There is no default: pass `--endpoint` or
132+
set `GRAYCODE_CLOUD_URL` to your Graycode Cloud worker URL before running
133+
`graycode cloud login`. `https://api.graycodeai.com` is the browser BFF and
134+
will reject a device token.
135+
131136
The export contains metadata and hashes, not prompts, tool arguments/results,
132137
policy reasons, verification evidence, or runtime output. Swift remains
133138
available separately as `graycode swift graph export`. Persisted chat sessions
@@ -483,6 +488,14 @@ You may keep a **personal** parent **`go.work`** that lists alternate clones on
483488
|---|---|---|
484489
| **graycode** | This repo | AI coding agent |
485490
| **graycode-router** | [GrayCodeAI/graycode-router](https://github.com/GrayCodeAI/graycode-router) | LLM provider runtime |
491+
| **graycode-skills** | [GrayCodeAI/graycode-skills](https://github.com/GrayCodeAI/graycode-skills) | Community skill registry |
492+
| **graycode-platform** | [GrayCodeAI/graycode-platform](https://github.com/GrayCodeAI/graycode-platform) | Web, BFF, and Graycode Cloud |
493+
494+
`ecosystem.yaml` is the canonical inventory of repositories cloned as
495+
siblings in this local workspace; tooling reads it rather than carrying its
496+
own repo-name list. `harrier`, `shrike`, `swift`, `kestrel`, `merlin` and
497+
`falcon` above are consumed as pinned `go.mod` module dependencies rather
498+
than local workspace clones, so they are not listed there.
486499

487500
For the consolidated repo map and the current-vs-proposed architecture diagrams, see [docs/architecture/graycode-current-vs-proposed.md](docs/architecture/graycode-current-vs-proposed.md).
488501
For execution-graph ownership, automatic capture seams, export/sync commands,

‎cmd/agent.go‎

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8-
"text/tabwriter"
98

109
"github.com/GrayCodeAI/graycode-cli/internal/multiagent/agents"
10+
"github.com/GrayCodeAI/graycode-cli/internal/theme"
1111
"github.com/spf13/cobra"
1212
"golang.org/x/text/cases"
1313
"golang.org/x/text/language"
@@ -81,13 +81,12 @@ func runAgentList(cmd *cobra.Command, _ []string) error {
8181
return nil
8282
}
8383
if len(all) == 0 {
84-
fmt.Printf("No agents found. Create one with: graycode agent create <name>\n")
85-
fmt.Printf("Agent directory: %s\n", agents.DefaultDir())
84+
fmt.Printf("%s\n", auditTint("No agents found. Create one with: graycode agent create <name>", textMuted))
85+
fmt.Printf("%s\n", auditTint("Agent directory: "+agents.DefaultDir(), textPrimary))
8686
return nil
8787
}
8888

89-
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
90-
_, _ = fmt.Fprintf(w, "NAME\tMODEL\tDESCRIPTION\n")
89+
rows := make([][]string, 0, len(all))
9190
for _, a := range all {
9291
model := a.Model
9392
if model == "" {
@@ -100,9 +99,9 @@ func runAgentList(cmd *cobra.Command, _ []string) error {
10099
desc = string(runes[:50]) + "..."
101100
}
102101
}
103-
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", a.Name, model, desc)
102+
rows = append(rows, []string{a.Name, model, desc})
104103
}
105-
return w.Flush()
104+
return theme.PrintTable(os.Stdout, []string{"NAME", "MODEL", "DESCRIPTION"}, rows)
106105
}
107106

108107
func runAgentCreate(_ *cobra.Command, args []string) error {
@@ -146,8 +145,8 @@ You are a specialized agent. Complete tasks according to your expertise.
146145
return err
147146
}
148147

149-
fmt.Printf("Created agent %q at %s\n", name, path)
150-
fmt.Printf("Edit the file to customize the system prompt.\n")
148+
fmt.Printf("%s\n", auditTint("Created agent "+name+" at ", doneGreen)+auditTint(path, textPrimary))
149+
fmt.Printf("%s\n", auditTint("Edit the file to customize the system prompt.", textMuted))
151150
return nil
152151
}
153152

@@ -157,15 +156,15 @@ func runAgentShow(_ *cobra.Command, args []string) error {
157156
return err
158157
}
159158

160-
fmt.Printf("Name: %s\n", a.Name)
161-
fmt.Printf("Description: %s\n", a.Description)
159+
fmt.Printf("%s %s\n", auditTint("Name:", textMuted), auditTint(a.Name, textPrimary))
160+
fmt.Printf("%s %s\n", auditTint("Description:", textMuted), auditTint(a.Description, textPrimary))
162161
model := a.Model
163162
if model == "" {
164163
model = "(inherit from session)"
165164
}
166-
fmt.Printf("Model: %s\n", model)
167-
fmt.Printf("File: %s\n", a.FilePath)
168-
fmt.Printf("\n--- Prompt ---\n%s\n", a.Prompt)
165+
fmt.Printf("%s %s\n", auditTint("Model:", textMuted), auditTint(model, textPrimary))
166+
fmt.Printf("%s %s\n", auditTint("File:", textMuted), auditTint(a.FilePath, textPrimary))
167+
fmt.Printf("\n%s\n%s\n", auditTint("--- Prompt ---", graycodeColor), a.Prompt)
169168
return nil
170169
}
171170

@@ -175,9 +174,18 @@ func runAgentRemove(_ *cobra.Command, args []string) error {
175174
return err
176175
}
177176

177+
ok, err := confirmDestructive(fmt.Sprintf("Remove agent %q (%s)?", a.Name, a.FilePath))
178+
if err != nil {
179+
return err
180+
}
181+
if !ok {
182+
fmt.Printf("%s\n", auditTint("Cancelled.", textMuted))
183+
return nil
184+
}
185+
178186
if err := os.Remove(a.FilePath); err != nil {
179187
return fmt.Errorf("remove %s: %w", a.FilePath, err)
180188
}
181-
fmt.Printf("Removed agent %q (%s)\n", a.Name, a.FilePath)
189+
fmt.Printf("%s\n", auditTint("Removed agent "+a.Name+" ("+a.FilePath+")", textPrimary))
182190
return nil
183191
}

‎cmd/ai_comments.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ func processAIDirectives(dir string, ignore []string) int {
150150
processed := 0
151151
for _, d := range directives {
152152
if err := aiDispatchFn(d); err != nil {
153-
fmt.Fprintf(os.Stderr, "AI directive %s:%d failed: %v\n", d.Path, d.Line, err)
153+
fmt.Fprintf(os.Stderr, "%s\n", auditTint(fmt.Sprintf("AI directive %s:%d failed: %v", d.Path, d.Line, err), errorCoral))
154154
continue
155155
}
156156
// Resolve back to an absolute path for removal; scan returns paths
@@ -160,7 +160,7 @@ func processAIDirectives(dir string, ignore []string) int {
160160
full = filepath.Join(dir, d.Path)
161161
}
162162
if err := removeAIComment(full, d.Line); err != nil {
163-
fmt.Fprintf(os.Stderr, "AI directive %s:%d: failed to strip token: %v\n", d.Path, d.Line, err)
163+
fmt.Fprintf(os.Stderr, "%s\n", auditTint(fmt.Sprintf("AI directive %s:%d: failed to strip token: %v", d.Path, d.Line, err), errorCoral))
164164
continue
165165
}
166166
processed++

‎cmd/audit.go‎

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package cmd
33
import (
44
"encoding/json"
55
"fmt"
6+
"image/color"
67
"os"
78
"path/filepath"
89
"sort"
910
"strings"
1011
"time"
1112

13+
lipgloss "charm.land/lipgloss/v2"
1214
"github.com/GrayCodeAI/graycode-cli/internal/hooks/audit"
1315
"github.com/GrayCodeAI/graycode-cli/internal/storage"
1416
"github.com/spf13/cobra"
@@ -65,6 +67,13 @@ type AuditResult struct {
6567
Detectors []AuditCount `json:"detectors"`
6668
}
6769

70+
// auditProgressEnabled reports whether per-session progress should be shown.
71+
// JSON output must stay pure (progress lines would corrupt it) and piped
72+
// text should not be spammed with per-session lines.
73+
func auditProgressEnabled(format string, tty bool) bool {
74+
return format != "json" && tty
75+
}
76+
6877
func runAudit(cmd *cobra.Command, args []string) error {
6978
if auditJSON {
7079
auditFormat = "json"
@@ -77,18 +86,37 @@ func runAudit(cmd *cobra.Command, args []string) error {
7786
}
7887

7988
if len(sessions) == 0 {
80-
cmd.Println("No session transcripts found for the specified time period.")
89+
cmd.Println(auditTint("No session transcripts found for the specified time period.", textMuted))
8190
return nil
8291
}
8392

93+
// Animated per-session progress only on a TTY and only for text output.
94+
// JSON output must stay pure (progress lines would corrupt it) and piped
95+
// text should not be spammed with per-session lines.
96+
var prog *CLIProgress
97+
if auditProgressEnabled(auditFormat, stdoutIsTerminal()) && !IsQuiet() {
98+
names := make([]string, len(sessions))
99+
for i := range sessions {
100+
names[i] = fmt.Sprintf("Scanning session %d/%d", i+1, len(sessions))
101+
}
102+
prog = NewCLIProgress("Audit", names)
103+
defer prog.Abort()
104+
}
105+
84106
// Run audit detectors on each session
85107
detectors := audit.AllDetectors()
86108
counts := make(map[string]*AuditCount)
87109
totalHits := 0
88110

89-
for _, sess := range sessions {
111+
for i, sess := range sessions {
112+
if prog != nil {
113+
prog.StartStep(i)
114+
}
90115
events, err := loadSessionEvents(sess.Path)
91116
if err != nil {
117+
if prog != nil {
118+
prog.FailStep(i, "load failed")
119+
}
92120
continue
93121
}
94122

@@ -125,6 +153,12 @@ func runAudit(cmd *cobra.Command, args []string) error {
125153
}
126154
}
127155
}
156+
if prog != nil {
157+
prog.CompleteStep(i)
158+
}
159+
}
160+
if prog != nil {
161+
prog.Done()
128162
}
129163

130164
// Count unique projects per detector
@@ -250,25 +284,54 @@ func loadSessionEvents(path string) ([]audit.ToolEvent, error) {
250284
return events, nil
251285
}
252286

287+
// auditTint applies a theme foreground color when color output is appropriate
288+
// (honors --quiet, NO_COLOR, FORCE_COLOR, and TTY state via ShouldColor).
289+
// Piped output stays plain so scripts never see stray ANSI escapes.
290+
func auditTint(s string, color color.Color) string {
291+
if !ShouldColor() || s == "" {
292+
return s
293+
}
294+
return lipgloss.NewStyle().Foreground(color).Render(s)
295+
}
296+
297+
// auditSeverityColor maps a detector severity to a semantic theme color.
298+
func auditSeverityColor(sev string) color.Color {
299+
switch sev {
300+
case "high", "critical":
301+
return errorCoral
302+
case "medium":
303+
return warnAmber
304+
default: // info, low
305+
return infoSky
306+
}
307+
}
308+
253309
func printAuditText(cmd *cobra.Command, result AuditResult) {
254310
w := cmd.OutOrStdout()
255311

256312
_, _ = fmt.Fprintf(w, "\n")
257313
_, _ = fmt.Fprintf(w, "═══════════════════════════════════════════════════════════════\n")
258-
_, _ = fmt.Fprintf(w, " Graycode Audit Report\n")
314+
_, _ = fmt.Fprintf(w, " %s\n", auditTint("Graycode Audit Report", graycodeColor))
259315
_, _ = fmt.Fprintf(w, "═══════════════════════════════════════════════════════════════\n")
260316
_, _ = fmt.Fprintf(w, "\n")
261-
_, _ = fmt.Fprintf(w, " Scanned: %d sessions (last %d days)\n", result.Sessions, result.Days)
262-
_, _ = fmt.Fprintf(w, " Total hits: %d\n", result.TotalHits)
263-
_, _ = fmt.Fprintf(w, " Scanned at: %s\n", result.ScannedAt)
317+
_, _ = fmt.Fprintf(w, " %s %d sessions (last %d days)\n",
318+
auditTint("Scanned:", textMuted), result.Sessions, result.Days)
319+
hitsColor := doneGreen
320+
if result.TotalHits > 0 {
321+
hitsColor = errorCoral
322+
}
323+
_, _ = fmt.Fprintf(w, " %s %s\n",
324+
auditTint("Total hits:", textMuted), auditTint(fmt.Sprintf("%d", result.TotalHits), hitsColor))
325+
_, _ = fmt.Fprintf(w, " %s %s\n",
326+
auditTint("Scanned at:", textMuted), auditTint(result.ScannedAt, textPrimary))
264327

265328
if len(result.Detectors) == 0 {
266-
_, _ = fmt.Fprintf(w, "\n No wasteful patterns detected. Great job!\n\n")
329+
_, _ = fmt.Fprintf(w, "\n %s\n\n", auditTint("No wasteful patterns detected. Great job!", doneGreen))
267330
return
268331
}
269332

270333
_, _ = fmt.Fprintf(w, "\n")
271-
_, _ = fmt.Fprintf(w, "─── Detected Patterns ───\n\n")
334+
_, _ = fmt.Fprintf(w, "─── %s ───\n\n", auditTint("Detected Patterns", infoSky))
272335
_, _ = fmt.Fprintf(w, " %-30s %6s %8s %s\n", "DETECTOR", "HITS", "SEVERITY", "EXAMPLE")
273336
_, _ = fmt.Fprintf(w, " %-30s %6s %8s %s\n", strings.Repeat("─", 30), strings.Repeat("─", 6), strings.Repeat("─", 8), strings.Repeat("─", 30))
274337

@@ -277,11 +340,14 @@ func printAuditText(cmd *cobra.Command, result AuditResult) {
277340
if len(d.Examples) > 0 {
278341
example = d.Examples[0]
279342
}
280-
_, _ = fmt.Fprintf(w, " %-30s %6d %8s %s\n", d.Name, d.Hits, d.Severity, example)
343+
// Pad to the column width first, then colorize, so ANSI escapes
344+
// (zero-width) don't break the fixed-width alignment.
345+
sev := auditTint(fmt.Sprintf("%8s", d.Severity), auditSeverityColor(d.Severity))
346+
_, _ = fmt.Fprintf(w, " %-30s %6d %s %s\n", d.Name, d.Hits, sev, example)
281347
}
282348

283349
_, _ = fmt.Fprintf(w, "\n")
284-
_, _ = fmt.Fprintf(w, "─── Remediation Tips ───\n\n")
350+
_, _ = fmt.Fprintf(w, "─── %s ───\n\n", auditTint("Remediation Tips", infoSky))
285351

286352
for _, d := range result.Detectors {
287353
switch d.Name {

‎cmd/audit_progress_test.go‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
// TestAuditProgressEnabled guards the JSON-purity contract: per-session
6+
// progress must never render when output is JSON (it would corrupt the
7+
// payload) or when stdout is piped (it would spam the report).
8+
func TestAuditProgressEnabled(t *testing.T) {
9+
cases := []struct {
10+
format string
11+
tty bool
12+
want bool
13+
}{
14+
{"json", true, false}, // JSON output must stay pure even on a TTY
15+
{"json", false, false}, // JSON + piped: never
16+
{"text", false, false}, // piped text: no per-session spam
17+
{"text", true, true}, // interactive text: animate
18+
{"", true, true}, // default format on a TTY: animate
19+
{"", false, false}, // default format piped: no spam
20+
}
21+
for _, c := range cases {
22+
if got := auditProgressEnabled(c.format, c.tty); got != c.want {
23+
t.Errorf("auditProgressEnabled(%q, %v) = %v, want %v", c.format, c.tty, got, c.want)
24+
}
25+
}
26+
}
27+
28+
// TestAuditSeverityColor verifies severity maps to the expected semantic
29+
// theme color so the report's severity column reads consistently.
30+
func TestAuditSeverityColor(t *testing.T) {
31+
if auditSeverityColor("high") != errorCoral {
32+
t.Error("high should map to errorCoral")
33+
}
34+
if auditSeverityColor("critical") != errorCoral {
35+
t.Error("critical should map to errorCoral")
36+
}
37+
if auditSeverityColor("medium") != warnAmber {
38+
t.Error("medium should map to warnAmber")
39+
}
40+
if auditSeverityColor("low") != infoSky {
41+
t.Error("low should map to infoSky")
42+
}
43+
if auditSeverityColor("info") != infoSky {
44+
t.Error("info should map to infoSky")
45+
}
46+
}

0 commit comments

Comments
 (0)