@hybridlabor-api/bdb-synapse 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -0
  3. package/bin/synapse +0 -0
  4. package/cmd/rubriceval/main.go +308 -0
  5. package/cmd/synapse/main.go +280 -0
  6. package/cmd/synapse/main_test.go +16 -0
  7. package/go.mod +7 -0
  8. package/go.sum +6 -0
  9. package/internal/adapter/adapter.go +1117 -0
  10. package/internal/adapter/adapter_test.go +518 -0
  11. package/internal/adapter/agy/adapter.go +193 -0
  12. package/internal/adapter/claudecode/adapter.go +415 -0
  13. package/internal/adapter/claudecode/adapter_test.go +260 -0
  14. package/internal/adapter/claudecode/agents.go +387 -0
  15. package/internal/adapter/claudecode/agents_test.go +480 -0
  16. package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
  17. package/internal/adapter/codex/adapter.go +919 -0
  18. package/internal/adapter/codex/adapter_test.go +920 -0
  19. package/internal/adapter/codex/agents.go +401 -0
  20. package/internal/adapter/codex/agents_test.go +610 -0
  21. package/internal/adapter/codex/summary_inputs_test.go +20 -0
  22. package/internal/adapter/pi/adapter.go +483 -0
  23. package/internal/adapter/pi/adapter_test.go +517 -0
  24. package/internal/citymap/builder.go +1124 -0
  25. package/internal/citymap/builder_test.go +818 -0
  26. package/internal/judge/cache.go +180 -0
  27. package/internal/judge/cli.go +240 -0
  28. package/internal/judge/cli_test.go +68 -0
  29. package/internal/judge/fresh_summary_test.go +37 -0
  30. package/internal/judge/input.go +233 -0
  31. package/internal/judge/judge.go +416 -0
  32. package/internal/judge/judge_test.go +288 -0
  33. package/internal/judge/prompt.go +129 -0
  34. package/internal/judge/rubric.go +275 -0
  35. package/internal/judge/rubric_test.go +642 -0
  36. package/internal/model/agent.go +63 -0
  37. package/internal/model/agent_schema_test.go +86 -0
  38. package/internal/model/agent_test.go +64 -0
  39. package/internal/model/model.go +175 -0
  40. package/internal/model/report.go +166 -0
  41. package/internal/model/stats.go +151 -0
  42. package/internal/model/stats_test.go +89 -0
  43. package/internal/model/trace_schema_test.go +67 -0
  44. package/internal/server/analyze.go +286 -0
  45. package/internal/server/analyze_test.go +297 -0
  46. package/internal/server/codex_index_test.go +40 -0
  47. package/internal/server/hardening_test.go +147 -0
  48. package/internal/server/reportindex.go +91 -0
  49. package/internal/server/reportindex_test.go +116 -0
  50. package/internal/server/server.go +1099 -0
  51. package/internal/server/server_test.go +1389 -0
  52. package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
  53. package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
  54. package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
  55. package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
  56. package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
  57. package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
  58. package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
  59. package/internal/server/static/assets/index-C_adLrJr.js +3 -0
  60. package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
  61. package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  62. package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  63. package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
  64. package/internal/server/static/index.html +26 -0
  65. package/internal/server/tracestore.go +173 -0
  66. package/internal/server/tracestore_test.go +51 -0
  67. package/internal/textutil/truncate.go +30 -0
  68. package/internal/textutil/truncate_test.go +39 -0
  69. package/package.json +35 -0
  70. package/web/e2e/agent-lens.spec.ts +688 -0
  71. package/web/index.html +23 -0
  72. package/web/package-lock.json +1933 -0
  73. package/web/package.json +33 -0
  74. package/web/playwright.config.ts +24 -0
  75. package/web/src/App.tsx +876 -0
  76. package/web/src/api/client.ts +74 -0
  77. package/web/src/main.tsx +12 -0
  78. package/web/src/playback/recorder.ts +160 -0
  79. package/web/src/playback/reducer.ts +91 -0
  80. package/web/src/scene/CityScene.tsx +638 -0
  81. package/web/src/scene/TreeScene.tsx +656 -0
  82. package/web/src/scene/dirLabels.ts +145 -0
  83. package/web/src/scene/sceneUtils.ts +144 -0
  84. package/web/src/scene/textures.ts +60 -0
  85. package/web/src/scene/trail.ts +79 -0
  86. package/web/src/scene/treeLayout.ts +169 -0
  87. package/web/src/state/filters.ts +40 -0
  88. package/web/src/state/store.ts +83 -0
  89. package/web/src/styles.css +2565 -0
  90. package/web/src/types.ts +315 -0
  91. package/web/src/ui/AgentsPanel.tsx +376 -0
  92. package/web/src/ui/Dock.tsx +104 -0
  93. package/web/src/ui/Hud.tsx +335 -0
  94. package/web/src/ui/Inspector.tsx +107 -0
  95. package/web/src/ui/LogoMark.tsx +38 -0
  96. package/web/src/ui/ReportPanel.tsx +491 -0
  97. package/web/src/ui/SessionRail.tsx +316 -0
  98. package/web/src/ui/Timeline.tsx +458 -0
  99. package/web/src/ui/ViewPanel.tsx +45 -0
  100. package/web/src/ui/shortcuts.ts +4 -0
  101. package/web/tsconfig.json +21 -0
  102. package/web/vite.config.ts +28 -0
@@ -0,0 +1,280 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "flag"
7
+ "fmt"
8
+ "os"
9
+ "path/filepath"
10
+
11
+ "github.com/hybridlabor-api/bdb-synapse/internal/adapter"
12
+ "github.com/hybridlabor-api/bdb-synapse/internal/adapter/claudecode"
13
+ "github.com/hybridlabor-api/bdb-synapse/internal/adapter/codex"
14
+ "github.com/hybridlabor-api/bdb-synapse/internal/adapter/pi"
15
+ "github.com/hybridlabor-api/bdb-synapse/internal/citymap"
16
+ "github.com/hybridlabor-api/bdb-synapse/internal/judge"
17
+ "github.com/hybridlabor-api/bdb-synapse/internal/model"
18
+ "github.com/hybridlabor-api/bdb-synapse/internal/server"
19
+ )
20
+
21
+ func main() {
22
+ if err := run(os.Args[1:]); err != nil {
23
+ fmt.Fprintln(os.Stderr, "synapse:", err)
24
+ os.Exit(1)
25
+ }
26
+ }
27
+
28
+ func run(args []string) error {
29
+ if len(args) == 0 {
30
+ return serve(args)
31
+ }
32
+ switch args[0] {
33
+ case "serve":
34
+ return serve(args[1:])
35
+ case "open":
36
+ return open(args[1:])
37
+ case "map":
38
+ return openMap(args[1:])
39
+ case "build":
40
+ return build(args[1:])
41
+ case "trace":
42
+ return trace(args[1:])
43
+ case "analyze":
44
+ return analyze(args[1:])
45
+ case "-h", "--help", "help":
46
+ usage()
47
+ return nil
48
+ default:
49
+ return fmt.Errorf("unknown command %q", args[0])
50
+ }
51
+ }
52
+
53
+ func serve(args []string) error {
54
+ fs := flag.NewFlagSet("serve", flag.ExitOnError)
55
+ port := fs.Int("port", 0, "port to bind on 127.0.0.1")
56
+ claudeDir := fs.String("claude-dir", claudecode.DefaultDir(), "Claude Code projects directory")
57
+ codexDir := fs.String("codex-dir", codex.DefaultDir(), "Codex sessions directory")
58
+ piDir := fs.String("pi-dir", pi.DefaultDir(), "pi sessions directory")
59
+ agyDir := fs.String("agy-dir", filepath.Join(os.Getenv("HOME"), ".gemini", "antigravity-cli", "brain"), "agy sessions directory")
60
+ dev := fs.Bool("dev", false, "prefer web/dist from the working tree")
61
+ noOpen := fs.Bool("no-open", false, "serve without opening a browser")
62
+ if err := fs.Parse(args); err != nil {
63
+ return err
64
+ }
65
+ return server.New(server.Config{Port: *port, ClaudeDir: *claudeDir, CodexDir: *codexDir, PiDir: *piDir, AgyDir: *agyDir, Dev: *dev}).Start(!*noOpen)
66
+ }
67
+
68
+ func open(args []string) error {
69
+ fs := flag.NewFlagSet("open", flag.ExitOnError)
70
+ port := fs.Int("port", 0, "port to bind on 127.0.0.1")
71
+ claudeDir := fs.String("claude-dir", claudecode.DefaultDir(), "Claude Code projects directory")
72
+ codexDir := fs.String("codex-dir", codex.DefaultDir(), "Codex sessions directory")
73
+ piDir := fs.String("pi-dir", pi.DefaultDir(), "pi sessions directory")
74
+ agyDir := fs.String("agy-dir", filepath.Join(os.Getenv("HOME"), ".gemini", "antigravity-cli", "brain"), "agy sessions directory")
75
+ noOpen := fs.Bool("no-open", false, "serve without opening a browser")
76
+ if err := fs.Parse(args); err != nil {
77
+ return err
78
+ }
79
+ if fs.NArg() != 1 {
80
+ return fmt.Errorf("usage: synapse open [--no-open] <session.jsonl>")
81
+ }
82
+ session, err := filepath.Abs(fs.Arg(0))
83
+ if err != nil {
84
+ return err
85
+ }
86
+ return server.New(server.Config{Port: *port, ClaudeDir: *claudeDir, CodexDir: *codexDir, PiDir: *piDir, AgyDir: *agyDir, OpenSession: session}).Start(!*noOpen)
87
+ }
88
+
89
+ func openMap(args []string) error {
90
+ fs := flag.NewFlagSet("map", flag.ExitOnError)
91
+ port := fs.Int("port", 0, "port to bind on 127.0.0.1")
92
+ dev := fs.Bool("dev", false, "prefer web/dist from the working tree")
93
+ noOpen := fs.Bool("no-open", false, "serve without opening a browser")
94
+ if err := fs.Parse(args); err != nil {
95
+ return err
96
+ }
97
+ if fs.NArg() != 1 {
98
+ return fmt.Errorf("usage: synapse map [--no-open] <repo>")
99
+ }
100
+ repo, err := filepath.Abs(fs.Arg(0))
101
+ if err != nil {
102
+ return err
103
+ }
104
+ return server.New(server.Config{Port: *port, Dev: *dev, RepoRoot: repo, MapOnly: true}).Start(!*noOpen)
105
+ }
106
+
107
+ func build(args []string) error {
108
+ positional, out, err := parseOutputArgs(args)
109
+ if err != nil {
110
+ return err
111
+ }
112
+ if len(positional) != 1 {
113
+ return fmt.Errorf("usage: synapse build <repo> [-o out]")
114
+ }
115
+ city, err := citymap.Builder{}.Build(positional[0], nil)
116
+ if err != nil {
117
+ return err
118
+ }
119
+ return writeJSON(out, city)
120
+ }
121
+
122
+ func trace(args []string) error {
123
+ positional, out, err := parseOutputArgs(args)
124
+ if err != nil {
125
+ return err
126
+ }
127
+ if len(positional) != 1 {
128
+ return fmt.Errorf("usage: synapse trace <session.jsonl> [-o out]")
129
+ }
130
+ tr, err := parseTrace(positional[0])
131
+ if err != nil {
132
+ return err
133
+ }
134
+ return writeJSON(out, tr)
135
+ }
136
+
137
+ // judgeMatches reports whether a cached report satisfies an explicit judge
138
+ // choice; unset flags match anything. A model matches on either the
139
+ // canonical name the run recorded (claude-sonnet-5) or the alias it was
140
+ // requested with (sonnet) — so repeating an aliased request hits the cache
141
+ // instead of paying for a fresh run every time.
142
+ func judgeMatches(report *model.Report, cli, modelName string) bool {
143
+ if cli != "" && report.Judge.CLI != cli {
144
+ return false
145
+ }
146
+ if modelName != "" && report.Judge.Model != modelName && report.Judge.RequestedModel != modelName {
147
+ return false
148
+ }
149
+ return true
150
+ }
151
+
152
+ func analyze(args []string) error {
153
+ fs := flag.NewFlagSet("analyze", flag.ExitOnError)
154
+ out := fs.String("o", "", "write the report to this file instead of stdout")
155
+ judgeCLI := fs.String("judge", "", "judge CLI to use: claude or codex (default: auto-detect)")
156
+ judgeModel := fs.String("model", "", "judge model override, e.g. sonnet or gpt-5.6-sol (default: the CLI's default)")
157
+ noCache := fs.Bool("no-cache", false, "re-run the judge even when a fresh cached report exists")
158
+ noRubric := fs.Bool("no-rubric", false, "skip the task rubric layer: one dimensions-only judge call, bypassing the report cache")
159
+ timeout := fs.Duration("timeout", judge.DefaultTimeout, "judge subprocess timeout")
160
+ // Accept flags after the positional argument, matching trace/build.
161
+ var positional []string
162
+ for {
163
+ if err := fs.Parse(args); err != nil {
164
+ return err
165
+ }
166
+ if fs.NArg() == 0 {
167
+ break
168
+ }
169
+ positional = append(positional, fs.Arg(0))
170
+ args = fs.Args()[1:]
171
+ }
172
+ if len(positional) != 1 {
173
+ return fmt.Errorf("usage: synapse analyze <session.jsonl> [-o out] [--judge claude|codex] [--model name] [--no-cache] [--no-rubric]")
174
+ }
175
+ session, err := filepath.Abs(positional[0])
176
+ if err != nil {
177
+ return err
178
+ }
179
+ tr, err := parseTrace(session)
180
+ if err != nil {
181
+ return err
182
+ }
183
+
184
+ cache := judge.Cache{Dir: judge.DefaultCacheDir()}
185
+ key := adapter.SessionKey(tr.Session.Harness, session)
186
+ // --no-cache means a fully fresh run: the cached report is neither
187
+ // returned nor mined for a reusable rubric. --no-rubric bypasses the
188
+ // cache in both directions — returning a cached rubric-ful report would
189
+ // contradict the flag, and storing a rubric-less one would downgrade a
190
+ // richer cache entry — so the flag always costs one fresh call.
191
+ var cached *model.Report
192
+ if !*noCache && !*noRubric {
193
+ cached = cache.Load(key)
194
+ // A rubric-enabled request is only answered from cache when the report
195
+ // already settles the rubric question; a rubric-less fresh report gets
196
+ // re-run rather than silently returned without the layer.
197
+ if judge.FreshAgainstTrace(cached, tr) && judgeMatches(cached, *judgeCLI, *judgeModel) &&
198
+ judge.RubricSatisfied(cached) {
199
+ fmt.Fprintln(os.Stderr, "synapse: using cached report (pass --no-cache to re-run)")
200
+ return writeJSON(*out, cached)
201
+ }
202
+ }
203
+
204
+ ctx, cancel := context.WithTimeout(context.Background(), *timeout)
205
+ defer cancel()
206
+ fmt.Fprintf(os.Stderr, "synapse: judging %d events, this can take a minute or two…\n", tr.Session.EventCount)
207
+ report, err := judge.Analyze(ctx, tr, judge.Options{CLI: *judgeCLI, Model: *judgeModel, NoRubric: *noRubric, CachedReport: cached})
208
+ if err != nil {
209
+ return err
210
+ }
211
+ if !*noRubric {
212
+ if err := cache.Store(key, report); err != nil {
213
+ fmt.Fprintln(os.Stderr, "synapse: report cache write failed:", err)
214
+ }
215
+ }
216
+ return writeJSON(*out, report)
217
+ }
218
+
219
+ func parseTrace(path string) (*model.Trace, error) {
220
+ var lastErr error
221
+ for _, source := range []adapter.Source{claudecode.Adapter{}, codex.Adapter{}, pi.Adapter{}} {
222
+ trace, err := source.Parse(path)
223
+ if err == nil {
224
+ return trace, nil
225
+ }
226
+ lastErr = err
227
+ }
228
+ if lastErr != nil {
229
+ return nil, lastErr
230
+ }
231
+ return nil, fmt.Errorf("no session adapters configured")
232
+ }
233
+
234
+ func parseOutputArgs(args []string) ([]string, string, error) {
235
+ var out string
236
+ var positional []string
237
+ for i := 0; i < len(args); i++ {
238
+ switch args[i] {
239
+ case "-o", "--output":
240
+ i++
241
+ if i >= len(args) {
242
+ return nil, "", fmt.Errorf("%s requires a value", args[i-1])
243
+ }
244
+ out = args[i]
245
+ default:
246
+ positional = append(positional, args[i])
247
+ }
248
+ }
249
+ return positional, out, nil
250
+ }
251
+
252
+ func writeJSON(out string, v any) error {
253
+ var f *os.File
254
+ var err error
255
+ if out == "" {
256
+ f = os.Stdout
257
+ } else {
258
+ f, err = os.Create(out)
259
+ if err != nil {
260
+ return err
261
+ }
262
+ defer f.Close()
263
+ }
264
+ enc := json.NewEncoder(f)
265
+ enc.SetIndent("", " ")
266
+ return enc.Encode(v)
267
+ }
268
+
269
+ func usage() {
270
+ fmt.Println(`synapse
271
+
272
+ Usage:
273
+ synapse serve on a random local port and open the UI
274
+ synapse serve [--port N] [--no-open] [--claude-dir DIR] [--codex-dir DIR] [--pi-dir DIR]
275
+ synapse open [--no-open] <session.jsonl> open a specific Claude Code, Codex, or pi session
276
+ synapse map [--no-open] <repo> open the repository citymap with no session
277
+ synapse build <repo> [-o out] write citymap.json
278
+ synapse trace <session> [-o out] write trace.json
279
+ synapse analyze <session> [-o out] [--judge claude|codex] [--no-cache] [--no-rubric] evaluate a session with a local agent CLI`)
280
+ }
@@ -0,0 +1,16 @@
1
+ package main
2
+
3
+ import "testing"
4
+
5
+ func TestParseOutputArgsAllowsTrailingOutput(t *testing.T) {
6
+ positional, out, err := parseOutputArgs([]string{"repo", "-o", "citymap.json"})
7
+ if err != nil {
8
+ t.Fatal(err)
9
+ }
10
+ if len(positional) != 1 || positional[0] != "repo" {
11
+ t.Fatalf("positional = %#v", positional)
12
+ }
13
+ if out != "citymap.json" {
14
+ t.Fatalf("out = %q", out)
15
+ }
16
+ }
package/go.mod ADDED
@@ -0,0 +1,7 @@
1
+ module github.com/hybridlabor-api/bdb-synapse
2
+
3
+ go 1.25
4
+
5
+ require github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
6
+
7
+ require golang.org/x/text v0.14.0 // indirect
package/go.sum ADDED
@@ -0,0 +1,6 @@
1
+ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
2
+ github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
3
+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
4
+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
5
+ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
6
+ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=