Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions .agents/skills/validate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Run the full test matrix for harness-openshell. Use when asked to "

# Validate

Run the full test matrix. Skip steps that require unavailable infrastructure.
Run the full test and documentation matrix. Skip steps that require unavailable infrastructure.

## Steps

Expand Down Expand Up @@ -77,13 +77,41 @@ Check if CI is green for the current branch:
gh run list --branch $(git branch --show-current) --limit 3
```

### 9. Docs consistency

Check that README.md and SPEC.md accurately reference the commands
registered in main.go. Every primary command in main.go should appear
in both README.md and SPEC.md. No stale command references should exist.

```bash
# Commands registered in main.go (primary, not deprecated)
grep 'cmd.New.*Cmd' main.go | grep -v Hidden | grep -v Deprecated

# Check README references all primary commands
for cmd in apply get describe deploy stop start; do
grep -q "harness $cmd" README.md && echo "README: $cmd OK" || echo "README: $cmd MISSING"
done

# Check SPEC references all primary commands
for cmd in apply get describe deploy stop start; do
grep -q "harness $cmd" SPEC.md && echo "SPEC: $cmd OK" || echo "SPEC: $cmd MISSING"
done

# Check for stale references to removed commands
for cmd in "harness up" "harness create" "harness render"; do
grep -c "$cmd" README.md SPEC.md 2>/dev/null
done
```

Report any docs/code mismatches.

## Output

Report a summary table:

```
Validation Results
──────────────────
------------------
Build: PASS
Unit tests: PASS (6 packages)
Vet: PASS
Expand All @@ -92,4 +120,5 @@ Validation Results
OCP: PASS (10/10)
Kind: SKIP (kind not installed)
CI: GREEN (3/3 workflows)
Docs: PASS (all commands documented, no stale refs)
```
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,25 @@ harness apply [-f FILE] [--agent NAME] [--gateway NAME] [--attach] [--dry-run] [
harness deploy [local|ocp|kind]
Deploy or verify the gateway for a target.

harness get agents [-o table|json|yaml]
List running sandboxes. Wraps openshell sandbox list with
consistent structured output. Aliases: sandboxes, sandbox.

harness get providers [-o table|json|yaml]
List registered providers. Credentials never included in output.

harness get gateways [-o table|json|yaml]
List gateways. Aliases: gateway, gw.

harness describe <name>
Detailed status for a specific sandbox (phase, gateway, providers).

harness stop [NAME] / harness start [NAME]
Stop or start a sandbox without deleting it.

harness teardown [--sandboxes] [--providers] [--k8s]
Tear down resources. At least one flag required.
(Deprecated: will be replaced by 'harness delete')

harness status
Show sandbox status.
(Deprecated: will be replaced by 'harness get agents')
```

For sandbox connect/logs, use openshell directly:
Expand Down
18 changes: 17 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ Default is non-interactive (headless). Use `--attach` for TTY mode.

`--provider-refresh` deletes and recreates all providers.

### `harness get <resource> [-o table|json|yaml]`

List resources. Wraps `openshell` list commands with consistent structured output across resource types. `-o table` is the default. Credential values are never included in structured output.

| Subcommand | Aliases | What it lists |
|------------|---------|--------------|
| `get agents` | `sandboxes`, `sandbox` | Running sandboxes (name, phase) |
| `get providers` | `provider` | Registered providers (name only, no credentials) |
| `get gateways` | `gateway`, `gw` | Gateways (name, endpoint, active) |

These are convenience wrappers. For full details, use `openshell sandbox list`, `openshell provider list`, etc. directly.

### `harness describe <name>`

Show detailed status for a specific sandbox: phase, active gateway, and registered providers.

### `harness deploy [local|ocp|kind]`

Deploy or verify the gateway for a target. Reads `profiles/gateways/<target>.yaml`.
Expand All @@ -113,7 +129,7 @@ These commands still work but will be removed in a future release:
| Old command | Replacement | Notes |
|-------------|-------------|-------|
| `harness teardown` | `harness delete` (planned) | Flags: `--sandboxes`, `--providers`, `--k8s` |
| `harness status` | `harness get agents` (planned) | |
| `harness status` | `harness get agents` | |

## Config Files

Expand Down
62 changes: 62 additions & 0 deletions cmd/describe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cmd

import (
"fmt"

"github.com/robbycochran/harness-openshell/internal/gateway"
"github.com/robbycochran/harness-openshell/internal/status"
"github.com/spf13/cobra"
)

func NewDescribeCmd(harnessDir, cli string) *cobra.Command {
cmd := &cobra.Command{
Use: "describe [NAME]",
Short: "Show detailed status for a sandbox",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
gw := gateway.New(cli)

sandboxes, err := gw.SandboxStatus()
if err != nil {
return fmt.Errorf("listing sandboxes: %w", err)
}

var found *gateway.SandboxInfo
for i := range sandboxes {
if sandboxes[i].Name == name {
found = &sandboxes[i]
break
}
}
if found == nil {
return fmt.Errorf("sandbox %q not found", name)
}

status.Header(found.Name)
status.Infof("Phase: %s", found.Phase)

gateways, err := gw.GatewayList()
if err == nil {
for _, g := range gateways {
if g.Active {
status.Infof("Gateway: %s (%s)", g.Name, g.Endpoint)
break
}
}
}

providers, err := gw.ProviderList()
if err == nil && len(providers) > 0 {
status.Infof("Providers: %d registered", len(providers))
for _, p := range providers {
fmt.Printf(" - %s\n", p)
}
}

return nil
},
}

return cmd
}
187 changes: 187 additions & 0 deletions cmd/get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package cmd

import (
"fmt"

"github.com/robbycochran/harness-openshell/internal/gateway"
"github.com/spf13/cobra"
)

func NewGetCmd(harnessDir, cli string) *cobra.Command {
cmd := &cobra.Command{
Use: "get",
Short: "Display resources",
Long: "List sandboxes, providers, or gateways. Use -o json or -o yaml for machine-readable output.",
}

cmd.AddCommand(
newGetAgentsCmd(cli),
newGetProvidersCmd(cli),
newGetGatewaysCmd(cli),
)

return cmd
}

func newGetAgentsCmd(cli string) *cobra.Command {
var output string

cmd := &cobra.Command{
Use: "agents",
Aliases: []string{"sandboxes", "sandbox"},
Short: "List running sandboxes",
RunE: func(cmd *cobra.Command, args []string) error {
format, err := parseOutputFormat(output)
if err != nil {
return err
}

gw := gateway.New(cli)
sandboxes, err := gw.SandboxStatus()
if err != nil {
return fmt.Errorf("listing sandboxes: %w", err)
}

if len(sandboxes) == 0 {
if format == formatTable {
fmt.Println("No sandboxes running.")
} else {
return printStructured(format, []any{})
}
return nil
}

if format != formatTable {
type sandboxOut struct {
Name string `json:"name" yaml:"name"`
Phase string `json:"phase" yaml:"phase"`
}
out := make([]sandboxOut, len(sandboxes))
for i, s := range sandboxes {
out[i] = sandboxOut{Name: s.Name, Phase: s.Phase}
}
return printStructured(format, out)
}

rows := make([][]string, len(sandboxes))
for i, s := range sandboxes {
rows[i] = []string{s.Name, s.Phase}
}
printTable([]string{"Name", "Phase"}, rows)
return nil
},
}

cmd.Flags().StringVarP(&output, "output", "o", "", "Output format: table, json, or yaml")
return cmd
}

func newGetProvidersCmd(cli string) *cobra.Command {
var output string

cmd := &cobra.Command{
Use: "providers",
Aliases: []string{"provider"},
Short: "List registered providers",
RunE: func(cmd *cobra.Command, args []string) error {
format, err := parseOutputFormat(output)
if err != nil {
return err
}

gw := gateway.New(cli)
providers, err := gw.ProviderList()
if err != nil {
return fmt.Errorf("listing providers: %w", err)
}

if len(providers) == 0 {
if format == formatTable {
fmt.Println("No providers registered.")
} else {
return printStructured(format, []any{})
}
return nil
}

if format != formatTable {
type providerOut struct {
Name string `json:"name" yaml:"name"`
}
out := make([]providerOut, len(providers))
for i, p := range providers {
out[i] = providerOut{Name: p}
}
return printStructured(format, out)
}

rows := make([][]string, len(providers))
for i, p := range providers {
rows[i] = []string{p}
}
printTable([]string{"Name"}, rows)
return nil
},
}

cmd.Flags().StringVarP(&output, "output", "o", "", "Output format: table, json, or yaml")
return cmd
}

func newGetGatewaysCmd(cli string) *cobra.Command {
var output string

cmd := &cobra.Command{
Use: "gateways",
Aliases: []string{"gateway", "gw"},
Short: "List gateways",
RunE: func(cmd *cobra.Command, args []string) error {
format, err := parseOutputFormat(output)
if err != nil {
return err
}

gw := gateway.New(cli)
gateways, err := gw.GatewayList()
if err != nil {
return fmt.Errorf("listing gateways: %w", err)
}

if len(gateways) == 0 {
if format == formatTable {
fmt.Println("No gateways registered.")
} else {
return printStructured(format, []any{})
}
return nil
}

if format != formatTable {
type gwOut struct {
Name string `json:"name" yaml:"name"`
Endpoint string `json:"endpoint" yaml:"endpoint"`
Active bool `json:"active" yaml:"active"`
}
out := make([]gwOut, len(gateways))
for i, g := range gateways {
out[i] = gwOut{Name: g.Name, Endpoint: g.Endpoint, Active: g.Active}
}
return printStructured(format, out)
}

rows := make([][]string, len(gateways))
for i, g := range gateways {
active := ""
if g.Active {
active = "*"
}
rows[i] = []string{active + g.Name, g.Endpoint}
}
printTable([]string{"Name", "Endpoint"}, rows)
return nil
},
}

cmd.Flags().StringVarP(&output, "output", "o", "", "Output format: table, json, or yaml")
return cmd
}
Loading
Loading