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 .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"name": "ox",
"source": "./claude-plugin",
"description": "Team context, session recording, and AI coworker coordination for collaborative development",
"version": "0.7.2"
"version": "0.8.0"
}
]
}
2 changes: 1 addition & 1 deletion claude-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ox",
"description": "Team context, session recording, and AI coworker coordination for collaborative development",
"version": "0.7.2",
"version": "0.8.0",
"author": {
"name": "SageOx",
"email": "hi@sageox.ai"
Expand Down
8 changes: 4 additions & 4 deletions cmd/ox/doctor_display_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ func TestDisplayDoctorResults_DetailRendering(t *testing.T) {
// TestRunDoctorChecks_CategoryStructure verifies runDoctorChecks returns expected category structure
func TestRunDoctorChecks_CategoryStructure(t *testing.T) {
t.Parallel()
categories := getCachedDoctorChecks()
categories := getCachedDoctorChecks(t)

// should return at least the core categories
require.NotEmpty(t, categories, "runDoctorChecks() returned no categories")
Expand Down Expand Up @@ -472,7 +472,7 @@ func TestRunDoctorChecks_WithFixFlag(t *testing.T) {
require.NotEmpty(t, categoriesWithFix, "runDoctorChecks(true) returned no categories")

// use cached fix=false result
categoriesWithoutFix := getCachedDoctorChecks()
categoriesWithoutFix := getCachedDoctorChecks(t)
require.NotEmpty(t, categoriesWithoutFix, "runDoctorChecks(false) returned no categories")

// both should return similar category structure; minor differences acceptable
Expand All @@ -483,7 +483,7 @@ func TestRunDoctorChecks_WithFixFlag(t *testing.T) {
// TestRunDoctorChecks_ChecksHaveValidFields verifies all checks have required fields
func TestRunDoctorChecks_ChecksHaveValidFields(t *testing.T) {
t.Parallel()
categories := getCachedDoctorChecks()
categories := getCachedDoctorChecks(t)

for _, cat := range categories {
assert.NotEmpty(t, cat.name, "category has empty name")
Expand All @@ -502,7 +502,7 @@ func TestRunDoctorChecks_ChecksHaveValidFields(t *testing.T) {
// TestRunDoctorChecks_ConfigCheckWithChildren verifies config check adds children when passed
func TestRunDoctorChecks_ConfigCheckWithChildren(t *testing.T) {
t.Parallel()
categories := getCachedDoctorChecks()
categories := getCachedDoctorChecks(t)

var projectCat *checkCategory
for i := range categories {
Expand Down
21 changes: 14 additions & 7 deletions cmd/ox/doctor_git_repos.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,17 +647,24 @@ func checkGitRepoPaths(fix bool) checkResult {
return fixRepoPathIssues(gitRoot, localCfg, issues)
}

// check if all issues are "missing" and the project was just initialized.
// after ox init, the daemon may not have cloned repos yet -- this is expected,
// not a failure. downgrade to info so users don't see scary errors on first run.
allMissing := true
// during the post-init bootstrap window, the daemon is still cloning repos,
// so any of these states is a transient artifact of the clone in progress —
// not a user-actionable problem. downgrade to info so a fresh install doesn't
// surface scary errors. a partially-created or stale directory at the default
// path counts the same as no directory at all for grace purposes.
allTransient := true
for _, issue := range issues {
if issue.issue != "missing" {
allMissing = false
switch issue.issue {
case "missing", "empty-dir", "not-git-repo":
// transient — clone in progress or pre-clone state
default:
allTransient = false
}
if !allTransient {
break
}
}
if allMissing && isRecentlyInitialized(gitRoot) {
if allTransient && isRecentlyInitialized(gitRoot) {
return InfoCheck("git repo paths",
fmt.Sprintf("%d repo(s) syncing", len(issues)),
"Background sync is cloning repos. Run `ox doctor` again in a minute.")
Expand Down
22 changes: 7 additions & 15 deletions cmd/ox/doctor_guidance.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package main

import (
"context"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"

"github.com/sageox/ox/internal/config"
"github.com/sageox/ox/internal/endpoint"
)

func init() {
Expand All @@ -24,7 +22,12 @@ func init() {
}

// checkGuidanceFiles detects whether the old root-level DISTILL.md needs migration
// and whether the base guidance files exist. On fix, migrates and seeds as needed.
// and whether the base guidance files exist. On fix, migrates and seeds locally —
// remote propagation happens via the daemon's normal team-context sync, not from
// a synchronous push here. A push at this layer would (a) make the read-only
// `ox doctor` command touch the network (FixLevelAuto runs even without --fix),
// (b) hang tests that share the cached doctor run, and (c) block fast-tier CI on
// any machine with a daemon-discovered team context.
func checkGuidanceFiles(fix bool) checkResult {
gitRoot := findGitRoot()
if gitRoot == "" {
Expand All @@ -36,18 +39,7 @@ func checkGuidanceFiles(fix bool) checkResult {
return SkippedCheck("Guidance files", "no team context", "")
}

result := checkGuidanceFilesForPath(tc.Path, fix)

// push team context after seeding/migrating guidance files
if fix && result.passed {
ep := endpoint.GetForProject(gitRoot)
if err := pushTeamContext(context.Background(), tc.Path, ep); err != nil {
slog.Warn("failed to push team context after guidance fix", "error", err)
result = WarningCheck("Guidance files", "seeded but push failed", err.Error())
}
}

return result
return checkGuidanceFilesForPath(tc.Path, fix)
}

// checkGuidanceFilesForPath is the testable core of the guidance check.
Expand Down
34 changes: 34 additions & 0 deletions cmd/ox/doctor_guidance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os/exec"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -152,3 +153,36 @@ func TestCheckGuidanceFiles_NoTeamContext(t *testing.T) {

assert.True(t, result.skipped, "should skip when no team context")
}

// TestCheckGuidanceFiles_FixDoesNotPush is the regression test for ox-ydb0.
// Background: a previous version of checkGuidanceFiles called pushTeamContext
// whenever fix=true. Because the check is FixLevelAuto, shouldFix returns true
// even when the user did not pass --fix, which meant every run of `ox doctor`
// shelled out to `git push` against the team context remote. That:
// - made a read-only diagnostic command mutate the remote,
// - hung the test package under high parallelism on real network I/O,
// - blocked fast-tier CI on any machine with a daemon-discovered team context.
//
// Push propagation is the daemon's job; this check is local-only by contract.
// If anyone reintroduces a push from inside checkGuidanceFiles, this test wedges
// on the broken `origin` URL set below and fails the suite quickly.
func TestCheckGuidanceFiles_FixDoesNotPush(t *testing.T) {
tcPath := initTeamContextGitDir(t)

// add an unreachable origin so any attempted push would either fail fast
// or block; either way, the test budget catches it.
c := exec.Command("git", "remote", "add", "origin", "https://invalid.test.localhost:1/never.git")
c.Dir = tcPath
require.NoError(t, c.Run())

done := make(chan checkResult, 1)
go func() { done <- checkGuidanceFilesForPath(tcPath, true) }()

select {
case result := <-done:
assert.True(t, result.passed, "guidance check should pass after local seed")
assert.False(t, result.warning, "guidance check should not warn — push must not be attempted")
case <-time.After(5 * time.Second):
t.Fatal("guidance check exceeded 5s budget — a network push was likely reintroduced")
}
}
17 changes: 13 additions & 4 deletions cmd/ox/doctor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,21 @@ import (

// cachedDoctorChecks caches runDoctorChecks result for tests that only need to
// verify structure/behavior, not test multiple scenarios. This saves ~60s in test time.
//
// Skips in -short mode: a real runDoctorChecks shells out to git/auth/etc. and
// against a developer machine with a daemon-discovered team context it can hang
// the entire test package on a real network operation. Fast-tier callers should
// never depend on full doctor state.
var (
cachedCategories []checkCategory
cachedCategoriesOnce sync.Once
)

func getCachedDoctorChecks() []checkCategory {
func getCachedDoctorChecks(t *testing.T) []checkCategory {
t.Helper()
if testing.Short() {
t.Skip("short: full doctor pipeline shells out to git/auth — see .claude/rules/testing.md")
}
cachedCategoriesOnce.Do(func() {
cachedCategories = runDoctorChecks(doctorOptions{fix: false})
})
Expand Down Expand Up @@ -123,7 +132,7 @@ func setupTestGitRepo(t *testing.T) (string, func()) {
// are suppressed when daemon is not running
func TestDoctorSuppression_DaemonNotRunning(t *testing.T) {
t.Parallel()
categories := getCachedDoctorChecks()
categories := getCachedDoctorChecks(t)

// find the Daemon category
var daemonCat *checkCategory
Expand Down Expand Up @@ -155,7 +164,7 @@ func TestDoctorSuppression_DaemonNotRunning(t *testing.T) {
// are suppressed when not logged in
func TestDoctorSuppression_NotLoggedIn(t *testing.T) {
t.Parallel()
categories := getCachedDoctorChecks()
categories := getCachedDoctorChecks(t)

// find the SageOx Service category
var serviceCat *checkCategory
Expand Down Expand Up @@ -185,7 +194,7 @@ func TestDoctorSuppression_NotLoggedIn(t *testing.T) {
// is suppressed when not logged in
func TestDoctorSuppression_GitRepoPaths(t *testing.T) {
t.Parallel()
categories := getCachedDoctorChecks()
categories := getCachedDoctorChecks(t)

// find the Git Repository Health category
var gitCat *checkCategory
Expand Down
42 changes: 29 additions & 13 deletions cmd/ox/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.8.0] - 2026-05-12

### Added

**Modular team rules with first-class context-budget accounting**
- Team rules now live as one-file-per-concern under `<team-context>/agents/rules/<topic>.md` (subdirectories supported, walked recursively). Mirrors the muscle memory of Claude Code's `.claude/rules/` and Cursor's `.cursor/rules/`, scaled up to team scope. Frontmatter spec covers `name`, `description`, `repos`, `audience`, `visibility`, `status`, `from-discussion`. `visibility: always` rules are inlined in `ox agent prime`; `visibility: indexed` rules emit a catalog entry only and the agent reads them on demand. Backward-compat fallback to `coworkers/rules/` for any teams that adopted that location early.
- `ox agent prime` XML now reports a `<context-budget>` block split by content source (sageox / team / project, plus any future knowledge bubble like `user`). The split lets SageOx be measured on its own tool overhead instead of conflating it with team-authored content. The split flows through every layer: per-prime budget, per-heartbeat per-source aggregation, daemon-side cumulative tracking, and `ox agent list`'s per-source footer. The schema is open — adding a new knowledge bubble takes one new constant in `internal/prime/types.go` plus tagging emit sites; no IPC or daemon-schema changes required.
- `ox agent prime` XML now reports a `<context-budget>` block split by content source (sageox / team / project). The split lets SageOx be measured on its own tool overhead instead of conflating it with team-authored content. It flows through every layer: per-prime budget, per-heartbeat per-source aggregation, daemon-side cumulative tracking, and `ox agent list`'s per-source footer. The schema is open — adding a new content source takes one new constant in `internal/prime/types.go` plus tagging emit sites.
- New `<rule-promotion-guidance>` block in prime XML proactively coaches AI coworkers to ask before publishing a project-local rule team-wide ("this looks like it could apply to your whole team — want me to also add it under `<team-context>/agents/rules/`?"). Default to asking; never silently publish.
- New `<team-rules-budget>` block reports the running token cost of `always`-tier rules so teams self-regulate rule-library size.
- Regression-test guard on minimal-prime SageOx overhead (currently ~600 tokens, ceiling 1500). A future change that quietly adds 5K of `<instructions>` blocks itself on review.
Expand All @@ -29,23 +31,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
**Rules-support scaffolding for the remaining adapters**
- New `rules.go` files for `ox-adapter-codex`, `ox-adapter-amp`, `ox-adapter-aider`, `ox-adapter-gemini`, `ox-adapter-opencode`, and `ox-adapter-pi` — each documenting the May 2026 state of that agent's rules surface. None of these agents has a Claude-Code-style modular *behavioral* rules directory today (Codex's `.codex/rules/` is for Starlark execution policies, not behavioral content). The handlers are stub no-ops, NOT wired into `main.go`, and the adapters do NOT advertise `CapRulesInstaller`. When upstream adds modular rules, flipping the wiring on is a 3-line change per adapter.

**Knowledge Bubbles: one home for the knowledge your AI coworkers need**
- New `ox kb` command surfaces every knowledge source you have access to in one list. Run `ox kb list` to see your personal scratchpad, your profile, your team contexts, and the per-repo ledgers side by side. `ox kb show <slug>` opens a single bubble; `ox kb path <slug>` prints its local checkout path so `cd $(ox kb path notes)` Just Works. `bubble` and `bubbles` are accepted as aliases for `kb` so muscle memory works either way.
- Personal bubbles are surfaced for the first time. Every signed-in user gets a private, single-owner scratchpad provisioned automatically; it shows up in `ox kb list` and is loaded into `ox agent prime` so AI coworkers can read and reference it from session one.
- Per-project ergonomic symlinks land in `.sageox/kb/<slug>` (gitignored) so editors and file pickers surface the bubbles relevant to the project you're in — personal, profile, team contexts you belong to, and this repo's ledger. The canonical XDG checkout still lives under `~/.local/share/sageox/<endpoint>/kb/<kb_id>/`; the symlinks are derived state, refreshed by the daemon on every reconciliation.
- `ox doctor` learned three new checks: orphaned bubble directories that no longer match the API list, bubbles stuck in `provision-failed` lifecycle state, and bubbles whose last sync is more than an hour stale. All three are auto-fixable on `--fix`.
- The daemon now syncs bubbles on its existing 15s/60s cadence (split by mutation rate) and gardens the local store: revoked bubbles move to `bubbles/.trash/<kb_id>-<timestamp>/` for a seven-day grace period before deletion, so an accidental access revoke is recoverable without a full reclone.
- Set `OX_KB_DISABLE=1` to force the CLI and daemon to skip the kb API and fall back to legacy team-context + ledger sources only. Mirrors `OX_XDG_DISABLE`; intended as an operator escape hatch during the rollout, not for daily use.

### Changed

**Reference docs regenerated**
- `docs/reference/` is now in sync with current cobra command definitions. Adds `guide.mdx`, `session/repair-meta-summary.mdx`, `session/token-optimize.mdx`, and the new `kb/`, `kb/list.mdx`, `kb/show.mdx`, `kb/path.mdx` pages. Drops a stale `distill.mdx` that was never registered as a root command. The `teams.mdx` page now carries the deprecation note inline.
- `docs/reference/` is now in sync with current cobra command definitions. Adds `guide.mdx`, `session/repair-meta-summary.mdx`, and `session/token-optimize.mdx`. Drops a stale `distill.mdx` that was never registered as a root command.

**Adapter ergonomics**
- The Amp adapter now records sessions via a user-global `ox-bridge` plugin. No per-repo configuration needed — install once and every Amp session in every cloned repo is captured automatically.
- `adapter-pi` now detects its host agent's identity from the `PI_CODING_AGENT` environment variable instead of fragile process-name heuristics.
- `--format=json` is now accepted as a hidden alias for `--json` across the CLI, so scripts written against either flag work everywhere.

### Fixed

**Daemon CPU & resource hygiene**
- Eliminated four recurring hot-loop CPU patterns that could pin a core under steady-state idle. Affected paths: failed session-upload retry, project-watcher tear-down, IPC reconnect, and friction-event drain.
- Closed a file-descriptor leak that occurred when the daemon ended up watching a directory that turned out to be gitignored. Long-running daemons no longer accumulate FDs proportional to gitignored-subdir churn.

**Doctor accuracy**
- Credential checks now run after the post-EEQI bootstrap so doctor no longer flags freshly-rotated credentials as missing on the very next run; user-facing guidance was also corrected to point at the right remediation command.
- Doctor scan gained correct session scoping, automatic hydration of LFS-stub recordings, catalog-identity verification, and an append-only redaction trail so previously-redacted content stays redacted across re-scans.
- `ox doctor --force-session-uploads` now actually re-uploads past failed sessions instead of being a silent no-op.

**Session reliability**
- `ox session stop` now writes the prompt + pointer commit inline so finalize is atomic. Previously the two writes could interleave with daemon work and leave a session half-committed for up to a minute.

**Security & redaction**
- Additional credential-redaction patterns close gaps in friction-event sanitization and team-context git-URL handling. Strengthened path-traversal, auth, and LFS size-bound checks per the latest internal review.

### Deprecated
**Code search resilience**
- `codedb` self-heals a corrupt bleve sub-index without forcing a full reindex. On large repos this drops recovery time from "several hours" to "2–5 minutes."

**`ox teams` is now an alias for `ox kb list --type=team`**
- `ox teams` continues to work for one release and prints a one-line deprecation hint to stderr pointing at the canonical command. Existing `ox teams --json` consumers see a new `deprecated` field in the JSON envelope so tooling can detect the alias programmatically; the rest of the legacy shape is unchanged. The alias will be removed in a future release.
[0.8.0]: https://github.com/sageox/ox/releases/tag/v0.8.0

## [0.7.2] - 2026-05-04

Expand Down
2 changes: 1 addition & 1 deletion internal/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package version

// Version information set via ldflags during build
var (
Version = "0.7.2"
Version = "0.8.0"
BuildDate = "unknown"
GitCommit = "unknown"
)
Expand Down
Loading