diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a25e85af..8aa853cc 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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" } ] } diff --git a/claude-plugin/.claude-plugin/plugin.json b/claude-plugin/.claude-plugin/plugin.json index f8b0c318..5ef9f68d 100644 --- a/claude-plugin/.claude-plugin/plugin.json +++ b/claude-plugin/.claude-plugin/plugin.json @@ -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" diff --git a/cmd/ox/doctor_display_test.go b/cmd/ox/doctor_display_test.go index 39541ad2..8f146f43 100644 --- a/cmd/ox/doctor_display_test.go +++ b/cmd/ox/doctor_display_test.go @@ -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") @@ -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 @@ -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") @@ -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 { diff --git a/cmd/ox/doctor_git_repos.go b/cmd/ox/doctor_git_repos.go index 478c8e0c..cf9ab6d3 100644 --- a/cmd/ox/doctor_git_repos.go +++ b/cmd/ox/doctor_git_repos.go @@ -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.") diff --git a/cmd/ox/doctor_guidance.go b/cmd/ox/doctor_guidance.go index 603a28ad..7c8d4dcd 100644 --- a/cmd/ox/doctor_guidance.go +++ b/cmd/ox/doctor_guidance.go @@ -1,7 +1,6 @@ package main import ( - "context" "fmt" "log/slog" "os" @@ -9,7 +8,6 @@ import ( "strings" "github.com/sageox/ox/internal/config" - "github.com/sageox/ox/internal/endpoint" ) func init() { @@ -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 == "" { @@ -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. diff --git a/cmd/ox/doctor_guidance_test.go b/cmd/ox/doctor_guidance_test.go index 4dbb4641..c775c7f1 100644 --- a/cmd/ox/doctor_guidance_test.go +++ b/cmd/ox/doctor_guidance_test.go @@ -7,6 +7,7 @@ import ( "os/exec" "path/filepath" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -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") + } +} diff --git a/cmd/ox/doctor_test.go b/cmd/ox/doctor_test.go index 3fcf19b0..df988d2d 100644 --- a/cmd/ox/doctor_test.go +++ b/cmd/ox/doctor_test.go @@ -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}) }) @@ -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 @@ -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 @@ -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 diff --git a/cmd/ox/release_notes.md b/cmd/ox/release_notes.md index f53f6eb8..ff379e76 100644 --- a/cmd/ox/release_notes.md +++ b/cmd/ox/release_notes.md @@ -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 `/agents/rules/.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 `` 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 `` 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 `` 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 `/agents/rules/`?"). Default to asking; never silently publish. - New `` 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 `` blocks itself on review. @@ -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 ` opens a single bubble; `ox kb path ` 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/` (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//kb//`; 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/-/` 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 diff --git a/internal/version/version.go b/internal/version/version.go index 6760df6c..f9fc6c6c 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -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" )