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
2 changes: 1 addition & 1 deletion cli/cmd/ao/capabilities_truthfulness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestTruthfulnessSixCommandSliceReportsRealContracts(t *testing.T) {
}{
{"ao version", "arbitrary", "text", "pure", map[string]string{"0": "success", "1": "failure"}},
{"ao capabilities", "none", "structured", "pure", map[string]string{"0": "success", "1": "failure"}},
{"ao status", "arbitrary", "text", "filesystem,clock", map[string]string{"0": "success", "1": "failure"}},
{"ao status", "arbitrary", "text", "filesystem,environment,clock", map[string]string{"0": "success", "1": "failure"}},
{"ao doctor", "no-args", "none", "filesystem,process,network,environment,clock", map[string]string{"0": "success", "1": "failure"}},
{"ao gate check", "no-args", "text", "filesystem,process,environment,clock", map[string]string{"0": "success", "1": "failure", "2": "invalid-configuration"}},
{"ao config", "none", "text", "filesystem,environment", map[string]string{"0": "success", "1": "failure"}},
Expand Down
78 changes: 78 additions & 0 deletions cli/cmd/ao/status_composition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package main

import (
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"

"github.com/boshu2/agentops/cli/internal/evidence"
"gopkg.in/yaml.v3"
)

func TestStatusExplicitEvidenceRootComposed(t *testing.T) {
bin, root, cwd := aoBinary(t), t.TempDir(), t.TempDir()
if _, err := evidence.SnapshotIntent(root, []byte("selected evidence")); err != nil {
t.Fatal(err)
}
config := filepath.Join(t.TempDir(), "config.yaml")
if err := os.WriteFile(config, []byte("{}\n"), 0600); err != nil {
t.Fatal(err)
}
t.Setenv("AGENTOPS_CONFIG", config)
t.Setenv("AGENTOPS_OUTPUT", "table")
for _, format := range []string{"text", "json", "yaml"} {
args := []string{"status", "--evidence-root", root}
if format != "text" {
args = append(args, "-o", format)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
cmd := exec.CommandContext(ctx, bin, args...)
cmd.Dir = cwd
out, err := cmd.CombinedOutput()
cancel()
if err != nil {
t.Fatalf("status %s: %v\n%s", format, err, out)
}
if format == "text" {
if !strings.Contains(string(out), "Artifacts: 1 intents, 0 verdicts") {
t.Fatalf("text selected wrong evidence: %s", out)
}
continue
}
var report map[string]map[string]any
if format == "json" {
err = json.Unmarshal(out, &report)
} else {
err = yaml.Unmarshal(out, &report)
}
if err != nil {
t.Fatal(err)
}
loop := report["loop_evidence"]
want := any(float64(1))
if format == "yaml" {
want = 1
}
if loop["intent_artifacts"] != want || loop["state"] != "intent_is_latest_evidence" {
t.Fatalf("%s selected wrong evidence: %s", format, out)
}
}
}

func TestStatusExplicitEvidenceRootCapability(t *testing.T) {
entry := capabilityEntry(t, "ao status")
for _, flag := range entry.Flags {
if flag.Name == "evidence-root" {
if flag.Required || flag.Origin != "local" || entry.Effects != "filesystem,environment,clock" {
t.Fatalf("status root contract: %+v, %+v", flag, entry)
}
return
}
}
t.Fatal("status capabilities omit --evidence-root")
}
7 changes: 7 additions & 0 deletions cli/docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ Display the content-addressed intent and verdict evidence stored by AgentOps.
ao status [flags]
```

**Flags:**

```
--evidence-root string Existing explicit non-Git evidence directory (default: working directory's .agents/ao)
-h, --help help for status
```

---

### `ao version`
Expand Down
6 changes: 6 additions & 0 deletions cli/internal/commands/provenance/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,12 @@ a watermark + a prefix checksum. If the transcript's already-mined prefix change
(rollback) — borrowed from cass's incremental-index discipline (stale-is-usable,
recover loudly, never rebuild expensive state unnecessarily).

Checkpoint updates are atomic; new files use mode 0600. Symlinks and special
files are rejected. Existing permissions must match an atomic replacement;
unverifiable or different security metadata returns an error without replacing
the checkpoint. An error syncing the directory after replacement can leave the
new checkpoint visible. Events may already have been emitted before an error.

Output (--json, default): one JSON event per line on stdout. The events feed the
PROV-O graph via a downstream step (e.g. wired as an ASSAY --mine-cmd); this
command does not itself write the committed ledger.`,
Expand Down
34 changes: 24 additions & 10 deletions cli/internal/commands/status/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ func NewModule(host clicontract.HostOptions) Module {

// Contract declares status's real behavior: it accepts (and ignores) arbitrary
// positional args exactly as Cobra does today, emits text (JSON under -o json),
// reads the durable evidence stores on the filesystem and stamps recency from
// the clock, and exits 0 on success or 1 on a working-directory failure.
// reads the durable evidence stores and active Git environment boundaries,
// stamps recency from the clock, and exits 0 on success or 1 on invalid input
// or a root-resolution failure.
func (Module) Contract() clicontract.CommandContract {
return clicontract.CommandContract{
ID: "ao.status",
Profiles: clicontract.ProfileDefault | clicontract.ProfileLegacy | clicontract.ProfileCombined,
Args: clicontract.ArgsPolicy{Name: "arbitrary", Validate: cobra.ArbitraryArgs},
Output: clicontract.OutputText,
Effects: clicontract.EffectFilesystem | clicontract.EffectClock,
Effects: clicontract.EffectFilesystem | clicontract.EffectEnvironment | clicontract.EffectClock,
ExitClasses: map[int]clicontract.ExitClass{
0: clicontract.ExitSuccess,
1: clicontract.ExitFailure,
Expand All @@ -43,7 +44,8 @@ func (Module) Contract() clicontract.CommandContract {
// Command builds the `ao status` command. The RunE closure delegates entirely
// to statusapp so this module performs no direct effect.
func (module Module) Command() *cobra.Command {
return &cobra.Command{
var evidenceRoot string
command := &cobra.Command{
Use: "status",
Short: "Show durable AgentOps loop evidence",
Long: `Display the content-addressed intent and verdict evidence stored by AgentOps.
Expand All @@ -52,25 +54,37 @@ The command validates artifact names, content identity, and verdict.v2 shape
before counting evidence. It reports recency only; it does not infer an active
runtime phase, elapsed execution time, tool activity, retries, or remaining work.

Without --evidence-root, read .agents/ao under the working directory.
With --evidence-root, inspect only intents/sha256 and verdicts/sha256 in that
existing non-Git directory. Invalid roots fail without fallback or writes;
evidence directory and file symlinks are excluded from explicit-root inspection.

Examples:
ao status
ao status --json`,
ao status --json
ao status --evidence-root /path/to/evidence --json`,
GroupID: "core",
RunE: func(cmd *cobra.Command, _ []string) error {
opts := statusapp.RunOptions{
JSON: module.host.OutputMode() == "json", Stdout: cmd.OutOrStdout(),
}
if cmd.Flags().Changed("evidence-root") {
opts.EvidenceRoot = &evidenceRoot
}
if module.host.OutputMode() == "yaml" {
// statusapp emits exactly one JSON document to Stdout in JSON
// mode; capture it and re-emit as YAML so -o yaml is the same
// data yaml-marshaled rather than a silent human-table fallback.
var buf bytes.Buffer
if err := statusapp.Run(statusapp.RunOptions{JSON: true, Stdout: &buf}); err != nil {
opts.JSON, opts.Stdout = true, &buf
if err := statusapp.Run(opts); err != nil {
return err
}
return clicontract.JSONToYAML(cmd.OutOrStdout(), buf.Bytes())
}
return statusapp.Run(statusapp.RunOptions{
JSON: module.host.OutputMode() == "json",
Stdout: cmd.OutOrStdout(),
})
return statusapp.Run(opts)
},
}
command.Flags().StringVar(&evidenceRoot, "evidence-root", "", "Existing explicit non-Git evidence directory (default: working directory's .agents/ao)")
return command
}
163 changes: 161 additions & 2 deletions cli/internal/commands/status/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"

"github.com/boshu2/agentops/cli/internal/clicontract"
"github.com/boshu2/agentops/cli/internal/evidence"
"github.com/boshu2/agentops/cli/internal/statusapp"
"gopkg.in/yaml.v3"
)

// newTestModule builds the status module with a fixed output mode, constructing
Expand All @@ -19,6 +23,33 @@ func newTestModule(outputMode string) Module {
return NewModule(clicontract.HostOptions{OutputMode: func() string { return outputMode }})
}

func TestModule_ExplicitEvidenceRootFromUnrelatedDirectory(t *testing.T) {
root := t.TempDir()
if _, err := evidence.SnapshotIntent(root, []byte("selected external intent")); err != nil {
t.Fatal(err)
}
cwd := t.TempDir()
writeIntentArtifact(t, cwd, "unrelated intent one")
writeIntentArtifact(t, cwd, "unrelated intent two")
t.Chdir(cwd)

var buf bytes.Buffer
command := newTestModule("json").Command()
command.SetOut(&buf)
command.SetErr(&buf)
command.SetArgs([]string{"--evidence-root", root})
if err := command.Execute(); err != nil {
t.Fatalf("status --evidence-root: %v\n%s", err, buf.String())
}
var got statusapp.Output
if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
t.Fatal(err)
}
if got.LoopEvidence.IntentArtifacts != 1 || got.LoopEvidence.VerdictArtifacts != 0 {
t.Fatalf("selected evidence counts: %+v", got.LoopEvidence)
}
}

func TestModule_Contract(t *testing.T) {
contract := newTestModule("text").Contract()
if contract.ID != "ao.status" {
Expand All @@ -27,9 +58,137 @@ func TestModule_Contract(t *testing.T) {
if contract.Output != clicontract.OutputText {
t.Fatalf("output = %v, want OutputText", contract.Output)
}
if contract.Effects&clicontract.EffectFilesystem == 0 || contract.Effects&clicontract.EffectClock == 0 {
t.Fatalf("effects = %v, want filesystem+clock", contract.Effects)
if contract.Effects != clicontract.EffectFilesystem|clicontract.EffectEnvironment|clicontract.EffectClock {
t.Fatalf("effects = %v, want filesystem+environment+clock", contract.Effects)
}
}

func TestModule_ExplicitRootFormatsPreserveStructuralBoundary(t *testing.T) {
root := t.TempDir()
stored := storeExternalVerdict(t, root)
// Unrelated files and nested legacy stores must not join the selected stores.
writeIntentArtifact(t, root, "nested legacy intent")
if err := os.WriteFile(filepath.Join(root, "notes.json"), []byte("not evidence"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(filepath.Dir(stored.Path), strings.Repeat("a", 64)+".json"), []byte("{}"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(filepath.Dir(stored.IntentRef), "notes.txt"), []byte("not an intent"), 0600); err != nil {
t.Fatal(err)
}
t.Chdir(t.TempDir())
var reports []map[string]any
for _, format := range []string{"text", "json", "yaml"} {
var buf bytes.Buffer
command := newTestModule(format).Command()
command.SetOut(&buf)
command.SetArgs([]string{"--evidence-root", root})
if err := command.Execute(); err != nil {
t.Fatalf("%s: %v", format, err)
}
if format == "text" {
for _, want := range []string{"Artifacts: 1 intents, 1 verdicts", "Corrupt:", "Not checked:", "caller-supplied subject manifests"} {
if !strings.Contains(buf.String(), want) {
t.Errorf("text missing %q: %s", want, buf.String())
}
}
continue
}
var report map[string]any
if format == "yaml" {
var value any
if err := yaml.Unmarshal(buf.Bytes(), &value); err != nil {
t.Fatal(err)
}
payload, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
buf.Reset()
buf.Write(payload)
}
if err := json.Unmarshal(buf.Bytes(), &report); err != nil {
t.Fatal(err)
}
loop := report["loop_evidence"].(map[string]any)
if loop["intent_artifacts"] != float64(1) || loop["verdict_artifacts"] != float64(1) || len(loop["corrupt"].([]any)) != 2 {
t.Fatalf("%s counts or structural validation: %+v", format, loop)
}
if !reflect.DeepEqual(loop["checked"], []any{"intents/sha256", "verdicts/sha256"}) || len(loop["not_checked"].([]any)) != 5 {
t.Fatalf("%s inspection boundary: %+v", format, loop)
}
reports = append(reports, report)
}
if !reflect.DeepEqual(reports[0], reports[1]) {
t.Fatalf("JSON/YAML reports differ: %+v", reports)
}
}

func TestModule_InvalidExplicitRootNeverFallsBack(t *testing.T) {
cwd := t.TempDir()
writeIntentArtifact(t, cwd, "fallback must not be inspected")
t.Chdir(cwd)
gitRoot := t.TempDir()
if err := os.Mkdir(filepath.Join(gitRoot, ".git"), 0700); err != nil {
t.Fatal(err)
}
file := filepath.Join(t.TempDir(), "file")
if err := os.WriteFile(file, []byte("not a directory"), 0600); err != nil {
t.Fatal(err)
}
for _, root := range []string{"", " ", filepath.Join(t.TempDir(), "missing"), gitRoot, file} {
for _, format := range []string{"text", "json", "yaml"} {
var buf bytes.Buffer
command := newTestModule(format).Command()
command.SilenceUsage, command.SilenceErrors = true, true
command.SetOut(&buf)
command.SetErr(&buf)
command.SetArgs([]string{"--evidence-root", root})
if err := command.Execute(); err == nil || !strings.Contains(err.Error(), "invalid --evidence-root") {
t.Fatalf("root %q, %s: %v", root, format, err)
}
if buf.Len() != 0 {
t.Fatalf("invalid root emitted fallback report: %s", buf.String())
}
}
}
}

func storeExternalVerdict(t *testing.T, root string) *evidence.StoreResult {
t.Helper()
subject := t.TempDir()
if err := os.WriteFile(filepath.Join(subject, "value"), []byte("synthetic subject"), 0600); err != nil {
t.Fatal(err)
}
manifest, err := evidence.BuildManifest(subject, []string{"value"}, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
manifestPath, err := evidence.StoreDocument(root, "manifest.json", manifest)
if err != nil {
t.Fatal(err)
}
draft := map[string]any{
"verdict": "PASS", "criteria": []any{map[string]any{"id": "synthetic", "result": "PASS", "evidence_refs": []string{"unresolved:synthetic-check"}}},
"findings": []any{}, "evidence_refs": []string{"unresolved:synthetic-check"}, "checked": []string{"value"}, "not_checked": []string{}, "validated_at": "2026-07-14T00:00:00Z",
}
draftPath, err := evidence.StoreDocument(root, "draft.json", draft)
if err != nil {
t.Fatal(err)
}
intent, err := evidence.SnapshotIntent(root, []byte("synthetic acceptance"))
if err != nil {
t.Fatal(err)
}
stored, err := evidence.StoreVerdict(evidence.StoreOptions{
Root: subject, EvidenceRoot: root, SubjectManifest: manifestPath, Draft: draftPath, IntentSource: intent.IntentRef,
Facts: evidence.RuntimeFacts{AuthorContextID: "synthetic-author", ValidatorContextID: "synthetic-judge", FreshnessSource: "runtime", FreshnessAttesterID: "synthetic-runtime", ScopeResult: "PASS"},
})
if err != nil {
t.Fatal(err)
}
return stored
}

func TestModule_CommandAttributes(t *testing.T) {
Expand Down
Loading
Loading