From 5ed58187d0208f388ac42ed872b2ec35c7738c1e Mon Sep 17 00:00:00 2001 From: Vishnu KV Date: Wed, 29 Jul 2026 20:52:45 +0530 Subject: [PATCH 1/4] feat(agent): integrate praxis-harness TUI + headless behind experimental flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds praxis-harness as a Go module dependency and exposes two new cobra commands gated behind PRAXIS_EXPERIMENTAL=1 or --experimental: - praxis chat: interactive Bubble Tea TUI coding agent (tui.Run) - praxis run: headless one-shot runner (native.Run) The bridge lives in internal/agent/ — the single import boundary between praxis-cli's cobra world and praxis-harness's SDK. It maps cobra flags to the harness's tui.Config (via ParseFlags) and native.Run (via argv), keeping the two repos decoupled. Auth is two-layer and stays separate: control-plane credentials (~/.praxis/credentials) for CLI gateway calls, LLM provider credentials (~/.praxis/agent/auth.json) for the agent. The TUI's /login handles provider auth. Changes: - go.mod: Go 1.25, praxis-harness dep (local replace for dev) - internal/agent/: bridge package (ChatOptions, HeadlessArgs, flag mapping) - cmd/chat.go: praxis chat cobra command with --experimental gate - cmd/run.go: praxis run cobra command with --experimental gate - .github/workflows: Go 1.25 in CI + release - 10 unit tests for the bridge (flag mapping, experimental gate, logo) Verified: go build, go vet, gofmt, 527 tests pass. --- .github/workflows/ci.yml | 4 +- .github/workflows/release.yml | 2 +- cmd/chat.go | 105 +++++++++++++++ cmd/run.go | 110 +++++++++++++++ go.mod | 84 +++++++++++- go.sum | 246 ++++++++++++++++++++++++++++++++++ internal/agent/agent.go | 226 +++++++++++++++++++++++++++++++ internal/agent/agent_test.go | 142 ++++++++++++++++++++ 8 files changed, 914 insertions(+), 5 deletions(-) create mode 100644 cmd/chat.go create mode 100644 cmd/run.go create mode 100644 internal/agent/agent.go create mode 100644 internal/agent/agent_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28124fc..63a70a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: - go-version: "1.24" + go-version: "1.25" cache: true - name: gofmt @@ -42,7 +42,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: - go-version: "1.24" + go-version: "1.25" cache: true - uses: goreleaser/goreleaser-action@v7 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e0165d1..50a36e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-go@v6 with: - go-version: "1.24" + go-version: "1.25" cache: true - name: Verify HOMEBREW_TAP_TOKEN propagates + can write to homebrew-tap diff --git a/cmd/chat.go b/cmd/chat.go new file mode 100644 index 0000000..6d2b229 --- /dev/null +++ b/cmd/chat.go @@ -0,0 +1,105 @@ +package cmd + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/Facets-cloud/praxis-cli/internal/agent" + "github.com/Facets-cloud/praxis-cli/internal/render" + "github.com/spf13/cobra" +) + +var ( + chatExperimental bool + chatModel string + chatThinking string + chatPermission string + chatCwd string + chatSessionID string + chatSessionDir string + chatResume string + chatTeamName string + chatSafe bool + chatEphemeral bool + chatMcpConfig string + chatSettings string + chatProfile string + chatFallback string + chatPrompt string + chatMaxTurns int +) + +var chatCmd = &cobra.Command{ + Use: "chat", + Short: "Start the interactive Praxis coding agent (experimental)", + Long: `Launch a full-featured terminal coding agent — multi-turn conversations, +tool execution, MCP integration, session persistence, and streaming model output. + +This is an EXPERIMENTAL feature. Enable it with --experimental or +PRAXIS_EXPERIMENTAL=1. The agent runs in-process (no separate binary needed) +and shares the Praxis profile directory (~/.praxis/agent/). + +Authentication: the agent uses LLM provider credentials stored in +~/.praxis/agent/auth.json, separate from your Praxis control-plane credentials +in ~/.praxis/credentials. Run 'praxis chat' once and use the /login command +inside the TUI to authenticate with an AI provider (Anthropic, OpenAI, etc.).`, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + if chatExperimental { + agent.Enable() + } + if err := agent.CheckEnabled(); err != nil { + render.PrintError(os.Stderr, false, err.Error(), "", 1) + return err + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + opts := agent.ChatOptions{ + Model: chatModel, + Thinking: chatThinking, + PermissionMode: chatPermission, + Cwd: chatCwd, + SessionID: chatSessionID, + SessionDir: chatSessionDir, + Resume: chatResume, + TeamName: chatTeamName, + SafeMode: chatSafe, + Ephemeral: chatEphemeral, + McpConfig: chatMcpConfig, + SettingsPath: chatSettings, + Profile: chatProfile, + FallbackModels: chatFallback, + Prompt: chatPrompt, + MaxTurns: chatMaxTurns, + } + + return agent.RunChat(ctx, opts) + }, +} + +func init() { + chatCmd.Flags().BoolVar(&chatExperimental, "experimental", false, "enable experimental agent features") + chatCmd.Flags().StringVarP(&chatModel, "model", "m", "", "model fuzzy-match (opus, gpt-5.2, glm-5.2)") + chatCmd.Flags().StringVar(&chatThinking, "thinking", "high", "reasoning effort: off|minimal|low|medium|high|xhigh|max|ultra") + chatCmd.Flags().StringVar(&chatPermission, "permission-mode", "auto", "permission mode: ask|auto|yolo") + chatCmd.Flags().StringVar(&chatCwd, "cwd", ".", "working directory") + chatCmd.Flags().StringVar(&chatSessionID, "session-id", "", "fixed session ID to create or resume") + chatCmd.Flags().StringVar(&chatSessionDir, "session-dir", "", "directory for persisted sessions") + chatCmd.Flags().StringVar(&chatResume, "resume", "", "resume a session by its file path") + chatCmd.Flags().StringVar(&chatTeamName, "team-name", "", "resume an existing durable agent team") + chatCmd.Flags().BoolVar(&chatSafe, "safe", false, "safe mode: ask permissions, block destructive ops") + chatCmd.Flags().BoolVar(&chatEphemeral, "ephemeral", false, "do not persist the session (no resume)") + chatCmd.Flags().StringVar(&chatMcpConfig, "mcp-config", "", "explicit MCP config path") + chatCmd.Flags().StringVar(&chatSettings, "settings", "", "explicit settings.json for hooks") + chatCmd.Flags().StringVar(&chatProfile, "profile", "", "isolated Praxis profile") + chatCmd.Flags().StringVar(&chatFallback, "fallback-models", "", "comma-separated fallback model IDs") + chatCmd.Flags().StringVar(&chatPrompt, "prompt", "", "initial prompt to send after startup") + chatCmd.Flags().IntVar(&chatMaxTurns, "max-turns", 0, "max turns per prompt (0 = default)") + + rootCmd.AddCommand(chatCmd) +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..d63f252 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,110 @@ +package cmd + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/Facets-cloud/praxis-cli/internal/agent" + "github.com/Facets-cloud/praxis-cli/internal/render" + "github.com/spf13/cobra" +) + +var ( + runExperimental bool + runPrompt string + runPromptFile string + runModel string + runProvider string + runThinking string + runSession string + runForkFrom string + runCwd string + runResultJSON bool + runUsageJSON bool + runNoMCP bool + runMcpConfig string + runSettings string + runMaxTurns int + runMaxTokenBudget int +) + +var runCmd = &cobra.Command{ + Use: "run", + Short: "Run a one-shot prompt through the Praxis agent (experimental)", + Long: `Execute a single prompt headlessly: build the agent, run the prompt to +completion, print the result, and exit. Designed for scripting, CI, and +automation pipelines. + +This is an EXPERIMENTAL feature. Enable it with --experimental or +PRAXIS_EXPERIMENTAL=1. + +The prompt can be provided via --prompt, --prompt-file, or piped through stdin +with --prompt -. + +Examples: + praxis run --experimental -prompt "fix the failing test" + praxis run --experimental -prompt - < input.txt + praxis run --experimental -prompt-file task.txt -model opus + praxis run --experimental -prompt "refactor" -result-json`, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + if runExperimental { + agent.Enable() + } + if err := agent.CheckEnabled(); err != nil { + render.PrintError(os.Stderr, false, err.Error(), "", 1) + return err + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + ha := agent.HeadlessArgs{ + Prompt: runPrompt, + PromptFile: runPromptFile, + Model: runModel, + Provider: runProvider, + Thinking: runThinking, + Session: runSession, + ForkFrom: runForkFrom, + Cwd: runCwd, + ResultJSON: runResultJSON, + UsageJSON: runUsageJSON, + NoMCP: runNoMCP, + McpConfig: runMcpConfig, + SettingsPath: runSettings, + MaxTurns: runMaxTurns, + MaxTokenBudget: runMaxTokenBudget, + } + + exitCode := agent.RunHeadless(ctx, ha.ToNativeArgs()) + if exitCode != 0 { + os.Exit(exitCode) + } + return nil + }, +} + +func init() { + runCmd.Flags().BoolVar(&runExperimental, "experimental", false, "enable experimental agent features") + runCmd.Flags().StringVar(&runPrompt, "prompt", "", "prompt text (or use - to read from stdin)") + runCmd.Flags().StringVar(&runPromptFile, "prompt-file", "", "read the prompt from this file") + runCmd.Flags().StringVarP(&runModel, "model", "m", "", "model name") + runCmd.Flags().StringVar(&runProvider, "provider", "", "provider override (anthropic|openai|google|zai)") + runCmd.Flags().StringVar(&runThinking, "thinking", "high", "reasoning effort") + runCmd.Flags().StringVar(&runSession, "session", "", "session ID to create/load/resume") + runCmd.Flags().StringVar(&runForkFrom, "fork-from", "", "fork a source session into -session") + runCmd.Flags().StringVar(&runCwd, "cwd", ".", "working directory") + runCmd.Flags().BoolVar(&runResultJSON, "result-json", false, "emit final result as JSON") + runCmd.Flags().BoolVar(&runUsageJSON, "usage-json", false, "emit cumulative usage as JSON") + runCmd.Flags().BoolVar(&runNoMCP, "no-mcp", false, "disable MCP discovery") + runCmd.Flags().StringVar(&runMcpConfig, "mcp-config", "", "explicit MCP config path") + runCmd.Flags().StringVar(&runSettings, "settings", "", "explicit settings.json for hooks") + runCmd.Flags().IntVar(&runMaxTurns, "max-turns", 25, "maximum model/tool turns") + runCmd.Flags().IntVar(&runMaxTokenBudget, "max-token-budget", 0, "maximum total billed tokens") + + rootCmd.AddCommand(runCmd) +} diff --git a/go.mod b/go.mod index 273a51d..42e84fd 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,90 @@ module github.com/Facets-cloud/praxis-cli -go 1.24.2 +go 1.25.0 -require github.com/spf13/cobra v1.10.2 +require ( + github.com/Facets-cloud/praxis-harness v0.0.0-00010101000000-000000000000 + github.com/spf13/cobra v1.10.2 +) require ( + charm.land/bubbletea/v2 v2.0.8 // indirect + charm.land/lipgloss/v2 v2.0.5 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/aymanbagabas/go-nativeclipboard v0.1.3 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/creack/pty v1.1.24 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/dlclark/regexp2/v2 v2.2.1 // indirect + github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 // indirect + github.com/dop251/goja v0.0.0-20260701091749-b07b74453ea9 // indirect + github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/ebitengine/purego v0.10.0-alpha.4 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/evanw/esbuild v0.28.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.9.0 // indirect + github.com/go-git/go-git/v5 v5.19.1 // indirect + github.com/go-python/gpython v0.2.0 // indirect + github.com/go-rod/rod v0.116.2 // indirect + github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.24 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect + github.com/pkoukk/tiktoken-go v0.1.8 // indirect + github.com/pkoukk/tiktoken-go-loader v0.0.2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/spf13/pflag v1.0.9 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/ysmood/fetchup v0.2.3 // indirect + github.com/ysmood/goob v0.4.0 // indirect + github.com/ysmood/got v0.40.0 // indirect + github.com/ysmood/gson v0.7.3 // indirect + github.com/ysmood/leakless v0.9.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/image v0.41.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.39.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.73.4 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.53.0 // indirect + mvdan.cc/sh/v3 v3.13.1 // indirect + rsc.io/pdf v0.1.1 // indirect ) + +replace github.com/Facets-cloud/praxis-harness => /Users/vishnukv/facets/codebases/praxis-harness diff --git a/go.sum b/go.sum index a6ee3e0..9a95550 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,256 @@ +charm.land/bubbletea/v2 v2.0.8 h1:SxTJMhCAI3lbPmy4SgX5LWZ24AdINr4I6UEqzZvYJuY= +charm.land/bubbletea/v2 v2.0.8/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss= +charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= +charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aymanbagabas/go-nativeclipboard v0.1.3 h1:FmAWHPTwneAixu7uGDn3cL42xPlUCdNp2J8egMn3P1k= +github.com/aymanbagabas/go-nativeclipboard v0.1.3/go.mod h1:2o7MyZwwi4pmXXpOpvOS5FwaHyoCIUks0ktjUvB0EoE= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 h1:3FmWoGNWK4STvqg0O0Aeav2T7rodWJAPeF0QpH+8gFw= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7/go.mod h1:f/jRa757WUmaOZrbPspXymbg/GnbF+rwe4OLsG7aXYo= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0= +github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= +github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 h1:16iT9CBDOniJwFGPI41MbUDfEk74hFaKTqudrX8kenY= +github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217/go.mod h1:eIb+f24U+eWQCIsj9D/ah+MD9UP+wdxuqzsdLD+mhGM= +github.com/dop251/goja v0.0.0-20260701091749-b07b74453ea9 h1:q33zakIx+wEp1Ko5NpDyDBICuXL4JeHUaHbhPowcMEk= +github.com/dop251/goja v0.0.0-20260701091749-b07b74453ea9/go.mod h1:Sc+QOu1WruvaaeT/cxFez/pXHpI9ZDjg/E8QNfSVveI= +github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14 h1:3U8dTgyNBhEQ/GVw0jZW5q+93Zw2gAZPRWhJ9TwV3rM= +github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14/go.mod h1:Tb7Xxye4LX7cT3i8YLvmPMGCV92IOi4CDZvm/V8ylc0= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.10.0-alpha.4 h1:JzPbdf+cqbyT9sZtP4xnqelwUXwf7LvD8xKS6+ofTds= +github.com/ebitengine/purego v0.10.0-alpha.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/evanw/esbuild v0.28.1 h1:ds+yuRyUaZGx++GR56CrCeuXh8PVhVM4xq8v7PNELFc= +github.com/evanw/esbuild v0.28.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-python/gpython v0.2.0 h1:MW7m7pFnbpzHL88vhAdIhT1pgG1QUZ0Q5jcF94z5MBI= +github.com/go-python/gpython v0.2.0/go.mod h1:fUN4z1X+GFaOwPOoHOAM8MOPnh1NJatWo/cDqGlZDEI= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= +github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo= +github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= +github.com/pkoukk/tiktoken-go-loader v0.0.2 h1:LUKws63GV3pVHwH1srkBplBv+7URgmOmhSkRxsIvsK4= +github.com/pkoukk/tiktoken-go-loader v0.0.2/go.mod h1:4mIkYyZooFlnenDlormIo6cd5wrlUKNr97wp9nGgEKo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= +github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= +github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= +github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= +github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= +github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= +golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= +modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= +modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= +modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= +modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= +modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk= +mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/agent/agent.go b/internal/agent/agent.go new file mode 100644 index 0000000..c27f75b --- /dev/null +++ b/internal/agent/agent.go @@ -0,0 +1,226 @@ +// Package agent bridges the praxis-harness SDK (TUI + headless runner) into the +// praxis-cli cobra command tree. It is the single import boundary between the +// two repos: cobra commands in cmd/ call these helpers, which delegate to +// praxis-harness's tui.Run / native.Run. +// +// All agent functionality is gated behind an experimental flag (PRAXIS_EXPERIMENTAL +// env var or --experimental CLI flag). When disabled, the chat/run commands +// print a message telling the user how to opt in. +package agent + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + + "github.com/Facets-cloud/praxis-harness/ai" + native "github.com/Facets-cloud/praxis-harness/cli/native" + "github.com/Facets-cloud/praxis-harness/tui" +) + +// experimentalEnvVar gates the agent commands behind an opt-in. The flag is +// also exposed as --experimental on the cobra subcommands. +const experimentalEnvVar = "PRAXIS_EXPERIMENTAL" + +// ErrExperimentalDisabled is returned when the agent commands are invoked +// without the experimental flag. +var ErrExperimentalDisabled = errors.New("agent commands are experimental; set PRAXIS_EXPERIMENTAL=1 or pass --experimental to enable") + +// Enabled returns true if the experimental agent features are turned on. +// The env var is the single source of truth: the --experimental cobra flag +// sets it before any agent command runs. +func Enabled() bool { + return os.Getenv(experimentalEnvVar) == "1" || os.Getenv(experimentalEnvVar) == "true" +} + +// Enable sets the env var so downstream harness code sees the flag. +func Enable() { + _ = os.Setenv(experimentalEnvVar, "1") +} + +// ChatOptions are the flags for the interactive TUI agent (praxis chat). +type ChatOptions struct { + Model string + Thinking string + PermissionMode string + Cwd string + SessionID string + SessionDir string + Resume string + TeamName string + SafeMode bool + Ephemeral bool + McpConfig string + SettingsPath string + Profile string + FallbackModels string + Prompt string + MaxTurns int +} + +// RunChat launches the interactive Bubble Tea TUI. It maps ChatOptions to the +// harness's tui.Config (via tui.ParseFlags on a synthesized flag slice) and +// calls tui.Run. Blocks until the user exits the TUI. +func RunChat(ctx context.Context, opts ChatOptions) error { + args := chatOptsToArgs(opts) + cfg := tui.ParseFlags(args) + return tui.Run(ctx, cfg) +} + +// RunHeadless executes a one-shot prompt through the headless native runner. +// It delegates to native.Run with a synthesized argv, exactly like the prx run +// subcommand. Returns the exit code. +func RunHeadless(ctx context.Context, args []string) int { + argv := append([]string{"praxis"}, args...) + return native.Run(ctx, argv) +} + +// chatOptsToArgs converts ChatOptions to a []string that tui.ParseFlags can +// consume. tui.Config fields are unexported, so we go through ParseFlags rather +// than constructing a Config directly. This keeps the bridge resilient to +// Config field additions in the harness — new fields just need new flag +// entries here. +func chatOptsToArgs(opts ChatOptions) []string { + var args []string + if opts.Model != "" { + args = append(args, "-model", opts.Model) + } + if opts.Thinking != "" { + args = append(args, "-thinking", opts.Thinking) + } + if opts.PermissionMode != "" { + args = append(args, "-permission-mode", opts.PermissionMode) + } + if opts.Cwd != "" { + args = append(args, "-cwd", opts.Cwd) + } + if opts.SessionID != "" { + args = append(args, "-session-id", opts.SessionID) + } + if opts.SessionDir != "" { + args = append(args, "-session-dir", opts.SessionDir) + } + if opts.Resume != "" { + args = append(args, "-resume", opts.Resume) + } + if opts.TeamName != "" { + args = append(args, "-team-name", opts.TeamName) + } + if opts.SafeMode { + args = append(args, "-safe") + } + if opts.Ephemeral { + args = append(args, "-ephemeral") + } + if opts.McpConfig != "" { + args = append(args, "-mcp-config", opts.McpConfig) + } + if opts.SettingsPath != "" { + args = append(args, "-settings", opts.SettingsPath) + } + if opts.Profile != "" { + args = append(args, "-profile", opts.Profile) + } + if opts.FallbackModels != "" { + args = append(args, "-fallback-models", opts.FallbackModels) + } + if opts.Prompt != "" { + args = append(args, "-prompt", opts.Prompt) + } + if opts.MaxTurns > 0 { + args = append(args, "-max-turns", fmt.Sprintf("%d", opts.MaxTurns)) + } + return args +} + +// HeadlessArgs converts a set of headless options to the argv slice that +// native.Run expects (without the leading program name). +type HeadlessArgs struct { + Prompt string + PromptFile string + Model string + Provider string + Thinking string + Session string + ForkFrom string + Cwd string + ResultJSON bool + UsageJSON bool + NoMCP bool + McpConfig string + SettingsPath string + MaxTurns int + MaxTokenBudget int +} + +// ToNativeArgs converts HeadlessArgs to the flag slice that native.Run parses. +func (h HeadlessArgs) ToNativeArgs() []string { + var args []string + add := func(flag, val string) { + if val != "" { + args = append(args, flag, val) + } + } + add("-prompt", h.Prompt) + add("-prompt-file", h.PromptFile) + add("-model", h.Model) + add("-provider", h.Provider) + add("-thinking", h.Thinking) + add("-session", h.Session) + add("-fork-from", h.ForkFrom) + add("-cwd", h.Cwd) + add("-mcp-config", h.McpConfig) + add("-settings", h.SettingsPath) + if h.ResultJSON { + args = append(args, "-result-json") + } + if h.UsageJSON { + args = append(args, "-usage-json") + } + if h.NoMCP { + args = append(args, "-no-mcp") + } + if h.MaxTurns > 0 { + args = append(args, "-max-turns", fmt.Sprintf("%d", h.MaxTurns)) + } + if h.MaxTokenBudget > 0 { + args = append(args, "-max-token-budget", fmt.Sprintf("%d", h.MaxTokenBudget)) + } + return args +} + +// CheckEnabled is a convenience for cobra RunE functions: returns +// ErrExperimentalDisabled when the experimental flag is off, with a hint. +func CheckEnabled() error { + if Enabled() { + return nil + } + return fmt.Errorf("%w\n\n %s", ErrExperimentalDisabled, + strings.Join([]string{ + "To try the experimental Praxis coding agent:", + " PRAXIS_EXPERIMENTAL=1 praxis chat", + " praxis chat --experimental", + }, "\n ")) +} + +// Logo returns the Praxis pixel-art logo from the harness TUI package. +// Used by the help/branding output in praxis-cli. +func Logo() string { + return tui.PraxisLogo() +} + +// ResolveModel resolves a model name to its provider using the harness's +// model registry. Returns the resolved model ID and provider name. +func ResolveModel(model string) (provider, resolved string, err error) { + resolvedModel, resolvedProvider, ok := ai.ResolveModelName(model) + if !ok { + return "", "", fmt.Errorf("unknown model %q", model) + } + return resolvedProvider, resolvedModel, nil +} + +// Ensure context is referenced for the linter even if future callers +// don't use it yet — RunChat and RunHeadless take ctx. +var _ = context.Background diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go new file mode 100644 index 0000000..b9ff7f7 --- /dev/null +++ b/internal/agent/agent_test.go @@ -0,0 +1,142 @@ +package agent + +import ( + "os" + "testing" +) + +func TestEnabledDefaultOff(t *testing.T) { + old := os.Getenv(experimentalEnvVar) + os.Unsetenv(experimentalEnvVar) + defer os.Setenv(experimentalEnvVar, old) + + if Enabled() { + t.Fatal("Enabled() = true, want false when PRAXIS_EXPERIMENTAL is unset") + } +} + +func TestEnabledEnvVar(t *testing.T) { + old := os.Getenv(experimentalEnvVar) + os.Setenv(experimentalEnvVar, "1") + defer os.Setenv(experimentalEnvVar, old) + + if !Enabled() { + t.Fatal("Enabled() = false, want true when PRAXIS_EXPERIMENTAL=1") + } +} + +func TestEnabledEnvVarTrue(t *testing.T) { + old := os.Getenv(experimentalEnvVar) + os.Setenv(experimentalEnvVar, "true") + defer os.Setenv(experimentalEnvVar, old) + + if !Enabled() { + t.Fatal("Enabled() = false, want true when PRAXIS_EXPERIMENTAL=true") + } +} + +func TestCheckEnabledReturnsErrorWhenOff(t *testing.T) { + old := os.Getenv(experimentalEnvVar) + os.Unsetenv(experimentalEnvVar) + defer os.Setenv(experimentalEnvVar, old) + + err := CheckEnabled() + if err == nil { + t.Fatal("CheckEnabled() = nil, want error when experimental is off") + } +} + +func TestCheckEnabledReturnsNilWhenOn(t *testing.T) { + old := os.Getenv(experimentalEnvVar) + os.Setenv(experimentalEnvVar, "1") + defer os.Setenv(experimentalEnvVar, old) + + err := CheckEnabled() + if err != nil { + t.Fatalf("CheckEnabled() = %v, want nil when experimental is on", err) + } +} + +func TestChatOptsToArgs(t *testing.T) { + opts := ChatOptions{ + Model: "opus", + Thinking: "high", + PermissionMode: "auto", + Cwd: ".", + SafeMode: true, + MaxTurns: 10, + } + args := chatOptsToArgs(opts) + + want := []string{"-model", "opus", "-thinking", "high", "-permission-mode", "auto", "-cwd", ".", "-safe", "-max-turns", "10"} + if len(args) != len(want) { + t.Fatalf("chatOptsToArgs returned %v (len %d), want %v (len %d)", args, len(args), want, len(want)) + } + for i := range want { + if args[i] != want[i] { + t.Fatalf("args[%d] = %q, want %q (full args: %v)", i, args[i], want[i], args) + } + } +} + +func TestChatOptsToArgsEmpty(t *testing.T) { + args := chatOptsToArgs(ChatOptions{}) + if len(args) != 0 { + t.Fatalf("chatOptsToArgs(empty) = %v, want empty slice", args) + } +} + +func TestHeadlessArgsToNativeArgs(t *testing.T) { + ha := HeadlessArgs{ + Prompt: "hello", + Model: "opus", + Cwd: ".", + NoMCP: true, + MaxTurns: 5, + } + args := ha.ToNativeArgs() + + // Check key flags are present + hasFlag := func(flag string) bool { + for _, a := range args { + if a == flag { + return true + } + } + return false + } + + if !hasFlag("-prompt") { + t.Errorf("ToNativeArgs missing -prompt, got %v", args) + } + if !hasFlag("-model") { + t.Errorf("ToNativeArgs missing -model, got %v", args) + } + if !hasFlag("-cwd") { + t.Errorf("ToNativeArgs missing -cwd, got %v", args) + } + if !hasFlag("-no-mcp") { + t.Errorf("ToNativeArgs missing -no-mcp, got %v", args) + } + if !hasFlag("-max-turns") { + t.Errorf("ToNativeArgs missing -max-turns, got %v", args) + } +} + +func TestEnable(t *testing.T) { + old := os.Getenv(experimentalEnvVar) + os.Unsetenv(experimentalEnvVar) + defer os.Setenv(experimentalEnvVar, old) + + Enable() + if !Enabled() { + t.Fatal("Enable() did not set PRAXIS_EXPERIMENTAL") + } +} + +func TestLogoNotEmpty(t *testing.T) { + logo := Logo() + if logo == "" { + t.Fatal("Logo() returned empty string") + } +} From 449f513f76ec59c51524e01e1355ad37a982bf11 Mon Sep 17 00:00:00 2001 From: Vishnu KV Date: Wed, 29 Jul 2026 23:10:20 +0530 Subject: [PATCH 2/4] ci: fetch praxis-harness v0.1.0 via CI_GITHUB_TOKEN (HTTPS PAT) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switches from SSH deploy key to a fine-grained PAT (CI_GITHUB_TOKEN secret) for private module access. Simpler setup — no SSH keys to manage. All three workflows (ci build, goreleaser-check, release) now: 1. Set GOPRIVATE + GONOSUMDB for Facets-cloud/* 2. Configure git insteadOf to embed the PAT in HTTPS URLs go.mod: replace directive removed; requires praxis-harness v0.1.0. --- .github/workflows/ci.yml | 21 +++++++++++++++++++++ .github/workflows/release.yml | 8 ++++++++ go.mod | 4 +--- go.sum | 2 ++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63a70a2..b4a43b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,23 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 with: go-version: "1.25" cache: true + # praxis-harness is a private repo in the same org. The default + # GITHUB_TOKEN can't read cross-repo, so CI_GITHUB_TOKEN (a PAT with + # read access to Facets-cloud repos) authenticates module fetches. + - name: Configure private module access + env: + CI_GITHUB_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} + run: | + git config --global url."https://x-access-token:${CI_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/" + go env -w GOPRIVATE="github.com/Facets-cloud/*" + go env -w GONOSUMDB="github.com/Facets-cloud/*" + - name: gofmt run: | unformatted=$(gofmt -l .) @@ -44,6 +56,15 @@ jobs: with: go-version: "1.25" cache: true + + - name: Configure private module access + env: + CI_GITHUB_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} + run: | + git config --global url."https://x-access-token:${CI_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/" + go env -w GOPRIVATE="github.com/Facets-cloud/*" + go env -w GONOSUMDB="github.com/Facets-cloud/*" + - uses: goreleaser/goreleaser-action@v7 with: version: "~> v2" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 50a36e8..6a8882e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,14 @@ jobs: go-version: "1.25" cache: true + - name: Configure private module access + env: + CI_GITHUB_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} + run: | + git config --global url."https://x-access-token:${CI_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/" + go env -w GOPRIVATE="github.com/Facets-cloud/*" + go env -w GONOSUMDB="github.com/Facets-cloud/*" + - name: Verify HOMEBREW_TAP_TOKEN propagates + can write to homebrew-tap env: HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} diff --git a/go.mod b/go.mod index 42e84fd..559b70c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Facets-cloud/praxis-cli go 1.25.0 require ( - github.com/Facets-cloud/praxis-harness v0.0.0-00010101000000-000000000000 + github.com/Facets-cloud/praxis-harness v0.1.0 github.com/spf13/cobra v1.10.2 ) @@ -86,5 +86,3 @@ require ( mvdan.cc/sh/v3 v3.13.1 // indirect rsc.io/pdf v0.1.1 // indirect ) - -replace github.com/Facets-cloud/praxis-harness => /Users/vishnukv/facets/codebases/praxis-harness diff --git a/go.sum b/go.sum index 9a95550..8ca8bd3 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Facets-cloud/praxis-harness v0.1.0 h1:g7EkR4wFhcJWLqJBVnGS/LHyt8NK/eTVXK+wQEAJNw4= +github.com/Facets-cloud/praxis-harness v0.1.0/go.mod h1:KgNWo9U3CrzPJE+mzAY0WeG7Sbw7YO0FljOXezXiYww= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= From 9cdb3d486f685c33e57a63591660f02fb5ae258b Mon Sep 17 00:00:00 2001 From: Vishnu KV Date: Wed, 29 Jul 2026 23:43:26 +0530 Subject: [PATCH 3/4] fix: address CodeRabbit review comments - Fix single-dash flag examples in help text to double-dash (--prompt etc.) - Use t.Setenv instead of manual os.Getenv/Setenv cleanup (preserves original unset state, auto-restores on test completion) - Assert errors.Is(err, ErrExperimentalDisabled) not just nil check - Strengthen HeadlessArgs.ToNativeArgs test to verify flag values not just presence - go.mod replace directive already removed in prior commit (now v0.1.0) --- cmd/run.go | 8 ++-- internal/agent/agent_test.go | 78 ++++++++++++++++-------------------- 2 files changed, 38 insertions(+), 48 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index d63f252..0b2bc71 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -44,10 +44,10 @@ The prompt can be provided via --prompt, --prompt-file, or piped through stdin with --prompt -. Examples: - praxis run --experimental -prompt "fix the failing test" - praxis run --experimental -prompt - < input.txt - praxis run --experimental -prompt-file task.txt -model opus - praxis run --experimental -prompt "refactor" -result-json`, + praxis run --experimental --prompt "fix the failing test" + praxis run --experimental --prompt - < input.txt + praxis run --experimental --prompt-file task.txt --model opus + praxis run --experimental --prompt "refactor" --result-json`, SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index b9ff7f7..b8de8b6 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -1,56 +1,44 @@ package agent import ( - "os" + "errors" "testing" ) func TestEnabledDefaultOff(t *testing.T) { - old := os.Getenv(experimentalEnvVar) - os.Unsetenv(experimentalEnvVar) - defer os.Setenv(experimentalEnvVar, old) - + t.Setenv(experimentalEnvVar, "") if Enabled() { t.Fatal("Enabled() = true, want false when PRAXIS_EXPERIMENTAL is unset") } } func TestEnabledEnvVar(t *testing.T) { - old := os.Getenv(experimentalEnvVar) - os.Setenv(experimentalEnvVar, "1") - defer os.Setenv(experimentalEnvVar, old) - + t.Setenv(experimentalEnvVar, "1") if !Enabled() { t.Fatal("Enabled() = false, want true when PRAXIS_EXPERIMENTAL=1") } } func TestEnabledEnvVarTrue(t *testing.T) { - old := os.Getenv(experimentalEnvVar) - os.Setenv(experimentalEnvVar, "true") - defer os.Setenv(experimentalEnvVar, old) - + t.Setenv(experimentalEnvVar, "true") if !Enabled() { t.Fatal("Enabled() = false, want true when PRAXIS_EXPERIMENTAL=true") } } func TestCheckEnabledReturnsErrorWhenOff(t *testing.T) { - old := os.Getenv(experimentalEnvVar) - os.Unsetenv(experimentalEnvVar) - defer os.Setenv(experimentalEnvVar, old) - + t.Setenv(experimentalEnvVar, "") err := CheckEnabled() if err == nil { t.Fatal("CheckEnabled() = nil, want error when experimental is off") } + if !errors.Is(err, ErrExperimentalDisabled) { + t.Fatalf("CheckEnabled() err = %v, want ErrExperimentalDisabled", err) + } } func TestCheckEnabledReturnsNilWhenOn(t *testing.T) { - old := os.Getenv(experimentalEnvVar) - os.Setenv(experimentalEnvVar, "1") - defer os.Setenv(experimentalEnvVar, old) - + t.Setenv(experimentalEnvVar, "1") err := CheckEnabled() if err != nil { t.Fatalf("CheckEnabled() = %v, want nil when experimental is on", err) @@ -96,38 +84,40 @@ func TestHeadlessArgsToNativeArgs(t *testing.T) { } args := ha.ToNativeArgs() - // Check key flags are present - hasFlag := func(flag string) bool { - for _, a := range args { - if a == flag { - return true + // Verify each flag AND its value (not just presence). + type kv struct{ flag, val string } + want := []kv{ + {"-prompt", "hello"}, + {"-model", "opus"}, + {"-cwd", "."}, + {"-max-turns", "5"}, + } + for _, w := range want { + found := false + for i, a := range args { + if a == w.flag && i+1 < len(args) && args[i+1] == w.val { + found = true + break } } - return false - } - - if !hasFlag("-prompt") { - t.Errorf("ToNativeArgs missing -prompt, got %v", args) - } - if !hasFlag("-model") { - t.Errorf("ToNativeArgs missing -model, got %v", args) + if !found { + t.Errorf("ToNativeArgs missing %s %s, got %v", w.flag, w.val, args) + } } - if !hasFlag("-cwd") { - t.Errorf("ToNativeArgs missing -cwd, got %v", args) + // Boolean flags: verify -no-mcp is present (no value). + hasNoMCP := false + for _, a := range args { + if a == "-no-mcp" { + hasNoMCP = true + } } - if !hasFlag("-no-mcp") { + if !hasNoMCP { t.Errorf("ToNativeArgs missing -no-mcp, got %v", args) } - if !hasFlag("-max-turns") { - t.Errorf("ToNativeArgs missing -max-turns, got %v", args) - } } func TestEnable(t *testing.T) { - old := os.Getenv(experimentalEnvVar) - os.Unsetenv(experimentalEnvVar) - defer os.Setenv(experimentalEnvVar, old) - + t.Setenv(experimentalEnvVar, "") Enable() if !Enabled() { t.Fatal("Enable() did not set PRAXIS_EXPERIMENTAL") From d4266b7abfac6accf7cb4e05b351c66f3a982fd4 Mon Sep 17 00:00:00 2001 From: Vishnu KV Date: Thu, 30 Jul 2026 11:29:29 +0530 Subject: [PATCH 4/4] feat(agent): expose session dashboard via praxis chat --agents; bump harness v0.1.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit praxis-cli has its own 'praxis agents' command (lists installed agent files; called with --json by skills praxis installs into AI hosts), so the harness session dashboard cannot share that verb. Expose it as a start view of the agent command instead: 'praxis chat --agents'. internal/agent.ChatOptions.AgentsView emits a LEADING positional 'agents' into tui.ParseFlags, which only honors it as args[0] — any other position and the TUI silently opens a normal chat session. MarkFlagsMutuallyExclusive rejects --agents combined with --prompt / --resume / --session-id, since tui.loadDashboardApplication clears exactly those per row. Bump praxis-harness v0.1.0 -> v0.1.1 for the dashboard fixes that make this flag usable: multiline/ANSI/OSC session titles are flattened and sanitized at the label source, the dashboard body is clamped to its reserved height in physical lines (composer stays visible), and header counts share one partition with the group headers. --- cmd/chat.go | 17 ++++++- cmd/chat_test.go | 88 ++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- internal/agent/agent.go | 16 ++++++- internal/agent/agent_test.go | 22 +++++++++ 6 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 cmd/chat_test.go diff --git a/cmd/chat.go b/cmd/chat.go index 6d2b229..4af5142 100644 --- a/cmd/chat.go +++ b/cmd/chat.go @@ -13,6 +13,7 @@ import ( var ( chatExperimental bool + chatAgents bool chatModel string chatThinking string chatPermission string @@ -44,7 +45,12 @@ and shares the Praxis profile directory (~/.praxis/agent/). Authentication: the agent uses LLM provider credentials stored in ~/.praxis/agent/auth.json, separate from your Praxis control-plane credentials in ~/.praxis/credentials. Run 'praxis chat' once and use the /login command -inside the TUI to authenticate with an AI provider (Anthropic, OpenAI, etc.).`, +inside the TUI to authenticate with an AI provider (Anthropic, OpenAI, etc.). + +Start views: 'praxis chat' opens a single session; 'praxis chat --agents' opens +the session dashboard, which lists persisted sessions grouped by state and +creates or resumes them from its composer. (Note 'praxis agents' is unrelated: +it lists the agent files praxis installed into your AI hosts.)`, SilenceUsage: true, SilenceErrors: true, RunE: func(cmd *cobra.Command, args []string) error { @@ -60,6 +66,7 @@ inside the TUI to authenticate with an AI provider (Anthropic, OpenAI, etc.).`, defer stop() opts := agent.ChatOptions{ + AgentsView: chatAgents, Model: chatModel, Thinking: chatThinking, PermissionMode: chatPermission, @@ -84,6 +91,7 @@ inside the TUI to authenticate with an AI provider (Anthropic, OpenAI, etc.).`, func init() { chatCmd.Flags().BoolVar(&chatExperimental, "experimental", false, "enable experimental agent features") + chatCmd.Flags().BoolVar(&chatAgents, "agents", false, "start on the session dashboard instead of a single session") chatCmd.Flags().StringVarP(&chatModel, "model", "m", "", "model fuzzy-match (opus, gpt-5.2, glm-5.2)") chatCmd.Flags().StringVar(&chatThinking, "thinking", "high", "reasoning effort: off|minimal|low|medium|high|xhigh|max|ultra") chatCmd.Flags().StringVar(&chatPermission, "permission-mode", "auto", "permission mode: ask|auto|yolo") @@ -101,5 +109,12 @@ func init() { chatCmd.Flags().StringVar(&chatPrompt, "prompt", "", "initial prompt to send after startup") chatCmd.Flags().IntVar(&chatMaxTurns, "max-turns", 0, "max turns per prompt (0 = default)") + // The dashboard owns session identity: it picks the row to open and clears the + // startup prompt and resume path (harness tui.loadDashboardApplication). Refuse + // the combinations it would silently drop rather than ignoring the user's input. + chatCmd.MarkFlagsMutuallyExclusive("agents", "prompt") + chatCmd.MarkFlagsMutuallyExclusive("agents", "resume") + chatCmd.MarkFlagsMutuallyExclusive("agents", "session-id") + rootCmd.AddCommand(chatCmd) } diff --git a/cmd/chat_test.go b/cmd/chat_test.go new file mode 100644 index 0000000..e20d349 --- /dev/null +++ b/cmd/chat_test.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "bytes" + "strings" + "testing" +) + +// execChat drives the command the way a user does: through the ROOT command. +// chatCmd.Execute() would not run chat at all — cobra's ExecuteC starts with +// `if c.HasParent() { return c.Root().ExecuteC() }`, so invoking Execute on a +// registered subcommand re-runs root with root's args (in a test binary, the +// `go test` flags) and silently ignores SetArgs on the child. +func execChat(t *testing.T, args ...string) (string, error) { + t.Helper() + // Reset BEFORE the run, not in a t.Cleanup: cleanups registered by a helper run + // when the test ends, so a table-driven caller would otherwise inherit the flag + // marks of the previous case and assert against the wrong conflict. + resetChatFlagState() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetErr(&buf) + rootCmd.SetArgs(append([]string{"chat"}, args...)) + t.Cleanup(func() { + rootCmd.SetArgs(nil) + rootCmd.SetOut(nil) + rootCmd.SetErr(nil) + resetChatFlagState() + }) + err := rootCmd.Execute() + return buf.String(), err +} + +// resetChatFlagState restores each flag's default VALUE and clears the "was this +// flag set" mark cobra records on the shared command. Both matter: flag-group +// enforcement reads the marks, so a stale one manufactures a conflict nobody asked +// for — and a leftover help=true makes cobra's execute() return flag.ErrHelp before +// it ever validates, which ExecuteC turns into a nil error that looks like success. +func resetChatFlagState() { + chatAgents, chatPrompt, chatResume, chatSessionID = false, "", "", "" + for _, name := range []string{"agents", "prompt", "resume", "session-id", "help"} { + if flag := chatCmd.Flags().Lookup(name); flag != nil { + _ = flag.Value.Set(flag.DefValue) + flag.Changed = false + } + } +} + +// `praxis agents` is this CLI's installed-agent-file lister, and the skills praxis +// installs into AI hosts call it with --json. The harness session dashboard is +// therefore a START VIEW of the agent command, `praxis chat --agents`, and the help +// has to say so — otherwise the two meanings of "agents" are indistinguishable to +// a user who just wants the dashboard. +func TestChatCmd_AgentsFlagDocumentsDashboardStartView(t *testing.T) { + out, err := execChat(t, "--help") + if err != nil { + t.Fatalf("chat --help err = %v", err) + } + for _, want := range []string{ + "--agents", + "start on the session dashboard", + "'praxis agents' is unrelated", + } { + if !strings.Contains(out, want) { + t.Errorf("chat --help missing %q\nfull output:\n%s", want, out) + } + } +} + +// The dashboard chooses which session to open, and the harness clears the startup +// prompt, resume path, and session id for that row (tui.loadDashboardApplication). +// Silently dropping those flags would look like the agent ignoring the user, so the +// combination is refused before anything launches. +func TestChatCmd_AgentsRejectsSingleSessionIdentityFlags(t *testing.T) { + for _, conflicting := range [][]string{ + {"--agents", "--prompt", "fix the bug"}, + {"--agents", "--resume", "/tmp/session.jsonl"}, + {"--agents", "--session-id", "abc123"}, + } { + _, err := execChat(t, conflicting...) + if err == nil { + t.Fatalf("chat %v was accepted; want a mutually-exclusive-flag error", conflicting) + } + if !strings.Contains(err.Error(), "none of the others can be") { + t.Fatalf("chat %v err = %v, want a mutual exclusion error", conflicting, err) + } + } +} diff --git a/go.mod b/go.mod index 559b70c..e7a4852 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/Facets-cloud/praxis-cli go 1.25.0 require ( - github.com/Facets-cloud/praxis-harness v0.1.0 + github.com/Facets-cloud/praxis-harness v0.1.1 github.com/spf13/cobra v1.10.2 ) diff --git a/go.sum b/go.sum index 8ca8bd3..e1a08b1 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/Facets-cloud/praxis-harness v0.1.0 h1:g7EkR4wFhcJWLqJBVnGS/LHyt8NK/eTVXK+wQEAJNw4= -github.com/Facets-cloud/praxis-harness v0.1.0/go.mod h1:KgNWo9U3CrzPJE+mzAY0WeG7Sbw7YO0FljOXezXiYww= +github.com/Facets-cloud/praxis-harness v0.1.1 h1:DG4GSSgSSstqrTkJEIKpi/irC8f1EKntSHh2y9U9HZY= +github.com/Facets-cloud/praxis-harness v0.1.1/go.mod h1:KgNWo9U3CrzPJE+mzAY0WeG7Sbw7YO0FljOXezXiYww= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= diff --git a/internal/agent/agent.go b/internal/agent/agent.go index c27f75b..da814a1 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -42,6 +42,12 @@ func Enable() { // ChatOptions are the flags for the interactive TUI agent (praxis chat). type ChatOptions struct { + // AgentsView starts the TUI on its session dashboard instead of a single chat + // session — the same view the harness's own `prx agents` opens. It is a start + // view, not a separate mode: the dashboard opens rows in the same in-process + // client. Note `praxis agents` is a DIFFERENT, non-agent command in this CLI + // (it lists installed agent files), so the dashboard is exposed as a flag. + AgentsView bool Model string Thinking string PermissionMode string @@ -60,7 +66,8 @@ type ChatOptions struct { MaxTurns int } -// RunChat launches the interactive Bubble Tea TUI. It maps ChatOptions to the +// RunChat launches the interactive Bubble Tea TUI, on a single chat session or on +// the session dashboard (ChatOptions.AgentsView). It maps ChatOptions to the // harness's tui.Config (via tui.ParseFlags on a synthesized flag slice) and // calls tui.Run. Blocks until the user exits the TUI. func RunChat(ctx context.Context, opts ChatOptions) error { @@ -84,6 +91,13 @@ func RunHeadless(ctx context.Context, args []string) int { // entries here. func chatOptsToArgs(opts ChatOptions) []string { var args []string + // tui.ParseFlags selects the dashboard start view from a LEADING positional + // "agents", which it consumes before parsing flags. It must therefore be + // args[0]; anywhere else it is an unparsed trailing positional and the TUI + // silently opens a normal chat session instead. + if opts.AgentsView { + args = append(args, "agents") + } if opts.Model != "" { args = append(args, "-model", opts.Model) } diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go index b8de8b6..e2be72c 100644 --- a/internal/agent/agent_test.go +++ b/internal/agent/agent_test.go @@ -67,6 +67,28 @@ func TestChatOptsToArgs(t *testing.T) { } } +// The dashboard start view rides on a LEADING positional that tui.ParseFlags +// consumes before parsing flags. If "agents" is not args[0] the harness treats it +// as a trailing positional and silently opens a normal chat session, so the +// position — not merely the presence — is the contract under test. +func TestChatOptsToArgsPutsAgentsViewFirst(t *testing.T) { + args := chatOptsToArgs(ChatOptions{AgentsView: true, Model: "opus", Cwd: "/repo"}) + + want := []string{"agents", "-model", "opus", "-cwd", "/repo"} + if len(args) != len(want) { + t.Fatalf("chatOptsToArgs = %v, want %v", args, want) + } + for i := range want { + if args[i] != want[i] { + t.Fatalf("args[%d] = %q, want %q (full args: %v)", i, args[i], want[i], args) + } + } + + if plain := chatOptsToArgs(ChatOptions{Model: "opus"}); len(plain) == 0 || plain[0] == "agents" { + t.Fatalf("chat without --agents must not request the dashboard: %v", plain) + } +} + func TestChatOptsToArgsEmpty(t *testing.T) { args := chatOptsToArgs(ChatOptions{}) if len(args) != 0 {